Версия 1.1.0 Крупные исправления в логике редактора статьи. Корректный выход без редактирования, учет изменения галереи. Убраны лишние перересовки редактора TipTap.

This commit is contained in:
anibilag 2025-10-21 15:09:10 +03:00
parent 07a596ad1d
commit fcad77cf9d
5 changed files with 118 additions and 62 deletions

View File

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Культура двух Столиц</title> <title>Культура Двух Столиц</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,7 +1,7 @@
{ {
"name": "vite-react-typescript-starter", "name": "vite-react-typescript-starter",
"private": true, "private": true,
"version": "1.0.8", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { TipTapEditor } from './Editor/TipTapEditor'; import { TipTapEditor } from './Editor/TipTapEditor';
import { CoverImageUpload } from './ImageUpload/CoverImageUpload'; import { CoverImageUpload } from './ImageUpload/CoverImageUpload';
import { ImageUploader } from './ImageUpload/ImageUploader'; import { ImageUploader } from './ImageUpload/ImageUploader';
@ -71,6 +71,10 @@ export function ArticleForm({
const isAdmin = user?.permissions.isAdmin || false; const isAdmin = user?.permissions.isAdmin || false;
const showGallery = false; const showGallery = false;
// Используем useRef для отслеживания состояния инициализации
const isInitializingRef = useRef(false);
const initialDataRef = useRef<FormState | null>(null);
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [excerpt, setExcerpt] = useState(''); const [excerpt, setExcerpt] = useState('');
const [categoryId, setCategoryId] = useState(availableCategoryIds[0] || 1); const [categoryId, setCategoryId] = useState(availableCategoryIds[0] || 1);
@ -82,23 +86,24 @@ export function ArticleForm({
const [formNewImageUrl, setFormNewImageUrl] = useState(''); const [formNewImageUrl, setFormNewImageUrl] = useState('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasChanges, setHasChanges] = useState<boolean>(false); const [hasChanges, setHasChanges] = useState<boolean>(false);
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true); const [isInitialized, setIsInitialized] = useState(false); // Заменяем isInitialLoad и formReady
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Добавляем флаг для отслеживания отправки const [isSubmitting, setIsSubmitting] = useState<boolean>(false); // Добавляем флаг для отслеживания отправки
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // Состояние для модального окна const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // Состояние для модального окна
const [newRole, setNewRole] = useState(''); const [newRole, setNewRole] = useState('');
const [newAuthorId, setNewAuthorId] = useState(''); const [newAuthorId, setNewAuthorId] = useState('');
const [showAddAuthorModal, setShowAddAuthorModal] = useState(false); const [showAddAuthorModal, setShowAddAuthorModal] = useState(false);
const [formReady, setFormReady] = useState(false);
// Добавляем состояние для отслеживания загрузки галереи
//const [galleryLoaded, setGalleryLoaded] = useState(false);
// Изменяем логику отслеживания состояния загрузки
const [dataLoaded, setDataLoaded] = useState(false);
const { images: galleryImages, loading: galleryLoading, error: galleryError, addImage: addGalleryImage, updateImage: updateGalleryImage, deleteImage: deleteGalleryImage, reorderImages: reorderGalleryImages } = useGallery(editingId || ''); const { images: galleryImages, loading: galleryLoading, error: galleryError, addImage: addGalleryImage, updateImage: updateGalleryImage, deleteImage: deleteGalleryImage, reorderImages: reorderGalleryImages } = useGallery(editingId || '');
const [selectedAuthors, setSelectedAuthors] = useState<AuthorLink[]>([]); const [selectedAuthors, setSelectedAuthors] = useState<AuthorLink[]>([]);
useEffect(() => {
if (initialFormState) setFormReady(true);
}, [initialFormState]);
// Добавляем обработку ошибок // Добавляем обработку ошибок
useEffect(() => { useEffect(() => {
const handleUnhandledRejection = (event: PromiseRejectionEvent) => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
@ -120,16 +125,27 @@ export function ArticleForm({
}; };
}, []); }, []);
// Эффект для отслеживания загрузки всех данных
useEffect(() => { useEffect(() => {
if (editingId) { if (!editingId) {
setDisplayedImages(galleryImages); // Для новой статьи данные загружены сразу
} else { setDataLoaded(true);
setDisplayedImages([]); return;
} }
}, [editingId, galleryImages]);
// Для редактирования ждем загрузки галереи
if (!galleryLoading) {
setDataLoaded(true);
}
}, [editingId, galleryLoading]);
// Эффект для инициализации формы
useEffect(() => { useEffect(() => {
if (initialFormState) { // Если начальные данные не пришли или данные еще не загружены - выходим
if (!initialFormState || !dataLoaded) return;
isInitializingRef.current = true;
setTitle(initialFormState.title); setTitle(initialFormState.title);
setExcerpt(initialFormState.excerpt); setExcerpt(initialFormState.excerpt);
setCategoryId(initialFormState.categoryId); setCategoryId(initialFormState.categoryId);
@ -137,20 +153,67 @@ export function ArticleForm({
setCoverImage(initialFormState.coverImage); setCoverImage(initialFormState.coverImage);
setReadTime(initialFormState.readTime); setReadTime(initialFormState.readTime);
setContent(initialFormState.content); setContent(initialFormState.content);
setDisplayedImages(initialFormState.galleryImages || []);
// Для редактирования используем загруженные данные галереи
// Для новой статьи - пустой массив
const galleryData = editingId ? galleryImages : [];
// Нормализуем данные галереи
const normalizedGalleryImages = galleryData.map(img => ({
id: img.id,
url: img.url,
caption: img.caption || '',
alt: img.alt || '',
width: img.width || 0,
height: img.height || 0,
size: img.size || 0,
format: img.format,
}));
setDisplayedImages(normalizedGalleryImages);
setSelectedAuthors( setSelectedAuthors(
(initialFormState.authors || []).map(a => ({ (initialFormState.authors || []).map(a => ({
authorId: a.author.id, // 👈 добавить вручную authorId: a.author.id,
role: a.role, role: a.role,
author: a.author author: a.author
})) }))
); );
// console.log('Содержимое статьи при загрузке:', initialFormState.content);
}
}, [initialFormState]);
// Сохраняем начальное состояние с нормализованной галереей
initialDataRef.current = {
...initialFormState,
authors: (initialFormState.authors || []).map(a => ({
role: a.role,
author: a.author
})),
galleryImages: normalizedGalleryImages
};
setIsInitialized(true);
}, [initialFormState, dataLoaded, editingId, galleryImages]);
// Эффект для отслеживания изменений формы
useEffect(() => { useEffect(() => {
if (!initialFormState || !formReady) return; if (!isInitialized || !initialDataRef.current) return;
// Нормализуем текущее состояние для сравнения
const normalizedAuthors = selectedAuthors.map(sa => ({
role: sa.role,
author: sa.author
}));
// Нормализуем текущую галерею для сравнения
const normalizedCurrentGallery = displayedImages.map(img => ({
id: img.id,
url: img.url,
caption: img.caption || '',
alt: img.alt || '',
width: img.width || 0,
height: img.height || 0,
size: img.size || 0,
format: img.format,
}));
const currentState: FormState = { const currentState: FormState = {
title, title,
@ -160,36 +223,23 @@ export function ArticleForm({
coverImage, coverImage,
readTime, readTime,
content, content,
authors: selectedAuthors, authors: normalizedAuthors,
galleryImages: displayedImages, galleryImages: normalizedCurrentGallery,
}; };
// Проверяем заполнение обязательных полей
const areRequiredFieldsFilled = title.trim() !== '' && excerpt.trim() !== ''; const areRequiredFieldsFilled = title.trim() !== '' && excerpt.trim() !== '';
const hasFormChanges = Object.keys(initialFormState).some(key => { // Сравниваем текущее состояние с начальным
if (!formReady) return false; const hasFormChanges = !isEqual(currentState, initialDataRef.current);
if (key === 'galleryImages') {
//if (!showGallery) return false; // 💡 игнорировать при выключенной галерее
const isDifferent = JSON.stringify(currentState[key]) !== JSON.stringify(initialFormState[key]);
if (isInitialLoad && isDifferent) return false;
return isDifferent;
}
if (key === 'content') {
return JSON.stringify(currentState[key]) !== JSON.stringify(initialFormState[key]);
}
const currentValue = typeof currentState[key as keyof FormState] === 'number' ? String(currentState[key as keyof FormState]) : currentState[key as keyof FormState];
const initialValue = typeof initialFormState[key as keyof FormState] === 'number' ? String(initialFormState[key as keyof FormState]) : initialFormState[key as keyof FormState];
return !isEqual(currentValue, initialValue);
});
// Устанавливаем флаг изменений
setHasChanges(hasFormChanges && areRequiredFieldsFilled); setHasChanges(hasFormChanges && areRequiredFieldsFilled);
}, [
if (isInitialLoad) { title, excerpt, categoryId, cityId, coverImage,
setIsInitialLoad(false); readTime, content, selectedAuthors, displayedImages,
} isInitialized
}, [title, excerpt, categoryId, cityId, coverImage, readTime, content, selectedAuthors, displayedImages, initialFormState, isInitialLoad, formReady, showGallery]); ]);
const filteredAuthors = authors.filter( const filteredAuthors = authors.filter(
(a) => (a) =>

View File

@ -84,13 +84,20 @@ export function TipTapEditor({ initialContent, onContentChange, articleId }: Tip
setSelectedImage(null); setSelectedImage(null);
} }
onContentChange(editor.getHTML()); // Добавляем проверку, чтобы избежать лишних обновлений
const newContent = editor.getHTML();
if (newContent !== initialContent) {
onContentChange(newContent);
}
}, },
}); });
useEffect(() => { useEffect(() => {
if (editor && editor.getHTML() !== initialContent) { if (editor && editor.getHTML() !== initialContent) {
// Используем requestAnimationFrame для отложенного обновления
requestAnimationFrame(() => {
editor.commands.setContent(initialContent); editor.commands.setContent(initialContent);
});
} }
}, [initialContent, editor]); }, [initialContent, editor]);

View File

@ -60,7 +60,6 @@ export interface GalleryImage {
height: number; height: number;
size: number; size: number;
format: string; format: string;
} }
export interface Author { export interface Author {