Версия 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">
<head>
<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" />
<title>Культура двух Столиц</title>
<title>Культура Двух Столиц</title>
</head>
<body>
<div id="root"></div>

View File

@ -1,7 +1,7 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "1.0.8",
"version": "1.1.0",
"type": "module",
"scripts": {
"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 { CoverImageUpload } from './ImageUpload/CoverImageUpload';
import { ImageUploader } from './ImageUpload/ImageUploader';
@ -71,6 +71,10 @@ export function ArticleForm({
const isAdmin = user?.permissions.isAdmin || false;
const showGallery = false;
// Используем useRef для отслеживания состояния инициализации
const isInitializingRef = useRef(false);
const initialDataRef = useRef<FormState | null>(null);
const [title, setTitle] = useState('');
const [excerpt, setExcerpt] = useState('');
const [categoryId, setCategoryId] = useState(availableCategoryIds[0] || 1);
@ -82,23 +86,24 @@ export function ArticleForm({
const [formNewImageUrl, setFormNewImageUrl] = useState('');
const [error, setError] = useState<string | null>(null);
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 [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); // Состояние для модального окна
const [newRole, setNewRole] = useState('');
const [newAuthorId, setNewAuthorId] = useState('');
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 [selectedAuthors, setSelectedAuthors] = useState<AuthorLink[]>([]);
useEffect(() => {
if (initialFormState) setFormReady(true);
}, [initialFormState]);
// Добавляем обработку ошибок
useEffect(() => {
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
@ -120,37 +125,95 @@ export function ArticleForm({
};
}, []);
// Эффект для отслеживания загрузки всех данных
useEffect(() => {
if (editingId) {
setDisplayedImages(galleryImages);
} else {
setDisplayedImages([]);
if (!editingId) {
// Для новой статьи данные загружены сразу
setDataLoaded(true);
return;
}
}, [editingId, galleryImages]);
useEffect(() => {
if (initialFormState) {
setTitle(initialFormState.title);
setExcerpt(initialFormState.excerpt);
setCategoryId(initialFormState.categoryId);
setCityId(initialFormState.cityId);
setCoverImage(initialFormState.coverImage);
setReadTime(initialFormState.readTime);
setContent(initialFormState.content);
setDisplayedImages(initialFormState.galleryImages || []);
setSelectedAuthors(
(initialFormState.authors || []).map(a => ({
authorId: a.author.id, // 👈 добавить вручную
role: a.role,
author: a.author
}))
);
// console.log('Содержимое статьи при загрузке:', initialFormState.content);
// Для редактирования ждем загрузки галереи
if (!galleryLoading) {
setDataLoaded(true);
}
}, [initialFormState]);
}, [editingId, galleryLoading]);
// Эффект для инициализации формы
useEffect(() => {
if (!initialFormState || !formReady) return;
// Если начальные данные не пришли или данные еще не загружены - выходим
if (!initialFormState || !dataLoaded) return;
isInitializingRef.current = true;
setTitle(initialFormState.title);
setExcerpt(initialFormState.excerpt);
setCategoryId(initialFormState.categoryId);
setCityId(initialFormState.cityId);
setCoverImage(initialFormState.coverImage);
setReadTime(initialFormState.readTime);
setContent(initialFormState.content);
// Для редактирования используем загруженные данные галереи
// Для новой статьи - пустой массив
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(
(initialFormState.authors || []).map(a => ({
authorId: a.author.id,
role: a.role,
author: a.author
}))
);
// Сохраняем начальное состояние с нормализованной галереей
initialDataRef.current = {
...initialFormState,
authors: (initialFormState.authors || []).map(a => ({
role: a.role,
author: a.author
})),
galleryImages: normalizedGalleryImages
};
setIsInitialized(true);
}, [initialFormState, dataLoaded, editingId, galleryImages]);
// Эффект для отслеживания изменений формы
useEffect(() => {
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 = {
title,
@ -160,36 +223,23 @@ export function ArticleForm({
coverImage,
readTime,
content,
authors: selectedAuthors,
galleryImages: displayedImages,
authors: normalizedAuthors,
galleryImages: normalizedCurrentGallery,
};
// Проверяем заполнение обязательных полей
const areRequiredFieldsFilled = title.trim() !== '' && excerpt.trim() !== '';
const hasFormChanges = Object.keys(initialFormState).some(key => {
if (!formReady) return false;
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);
});
// Сравниваем текущее состояние с начальным
const hasFormChanges = !isEqual(currentState, initialDataRef.current);
// Устанавливаем флаг изменений
setHasChanges(hasFormChanges && areRequiredFieldsFilled);
if (isInitialLoad) {
setIsInitialLoad(false);
}
}, [title, excerpt, categoryId, cityId, coverImage, readTime, content, selectedAuthors, displayedImages, initialFormState, isInitialLoad, formReady, showGallery]);
}, [
title, excerpt, categoryId, cityId, coverImage,
readTime, content, selectedAuthors, displayedImages,
isInitialized
]);
const filteredAuthors = authors.filter(
(a) =>

View File

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

View File

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