feat: Update article modal and left widget naming
This commit is contained in:
@@ -71,10 +71,8 @@ export const clearBlobAndGLTFCache = async (url: string) => {
|
|||||||
*/
|
*/
|
||||||
export const clearMediaTransitionCache = async (
|
export const clearMediaTransitionCache = async (
|
||||||
previousMediaId: string | number | null,
|
previousMediaId: string | number | null,
|
||||||
newMediaId: string | number | null,
|
|
||||||
newMediaType?: number
|
newMediaType?: number
|
||||||
) => {
|
) => {
|
||||||
console.log(newMediaId, newMediaType);
|
|
||||||
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
// Если переключаемся с/на 3D модель, очищаем весь кеш
|
||||||
if (newMediaType === 6 || previousMediaId) {
|
if (newMediaType === 6 || previousMediaId) {
|
||||||
await clearAllGLTFCache();
|
await clearAllGLTFCache();
|
||||||
|
|||||||
@@ -523,42 +523,60 @@ export const ArticleSelectOrCreateDialog = observer(
|
|||||||
article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
|
article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const [hoveredArticleId, setHoveredArticleId] = useState<string | null>(
|
// Preview-by-click logic with request serialization to avoid concurrent requests
|
||||||
null
|
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||||||
);
|
const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
|
||||||
const hoverTimerRef = (typeof window !== "undefined"
|
const clickTimerRef = (typeof window !== "undefined"
|
||||||
? (window as any)
|
? (window as any)
|
||||||
: {}) as {
|
: {}) as {
|
||||||
current?: any;
|
current?: any;
|
||||||
} as React.MutableRefObject<NodeJS.Timeout | null>;
|
} as React.MutableRefObject<NodeJS.Timeout | null>;
|
||||||
if (hoverTimerRef.current === undefined) {
|
if (clickTimerRef.current === undefined) {
|
||||||
(hoverTimerRef as any).current = null;
|
(clickTimerRef as any).current = null;
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
|
||||||
if (
|
const runPreviewFetch = async (articleId: number) => {
|
||||||
hoveredArticleId &&
|
if (isPreviewLoading) {
|
||||||
tabValue === 0 &&
|
setQueuedPreviewId(articleId);
|
||||||
!selectedArticleId &&
|
return;
|
||||||
!tempArticleId
|
|
||||||
) {
|
|
||||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
||||||
hoverTimerRef.current = setTimeout(() => {
|
|
||||||
getArticle(Number(hoveredArticleId), modalLanguage);
|
|
||||||
getArticleMedia(Number(hoveredArticleId));
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
return () => {
|
setIsPreviewLoading(true);
|
||||||
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
try {
|
||||||
};
|
await Promise.all([
|
||||||
}, [
|
getArticle(articleId, modalLanguage),
|
||||||
hoveredArticleId,
|
getArticleMedia(articleId),
|
||||||
tabValue,
|
]);
|
||||||
selectedArticleId,
|
} finally {
|
||||||
tempArticleId,
|
setIsPreviewLoading(false);
|
||||||
modalLanguage,
|
if (queuedPreviewId && queuedPreviewId !== articleId) {
|
||||||
getArticle,
|
const nextId = queuedPreviewId;
|
||||||
getArticleMedia,
|
setQueuedPreviewId(null);
|
||||||
]);
|
// Run the next queued preview
|
||||||
|
runPreviewFetch(nextId);
|
||||||
|
} else {
|
||||||
|
setQueuedPreviewId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListItemClick = (articleId: number) => {
|
||||||
|
// Delay to allow double-click to cancel preview
|
||||||
|
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
|
||||||
|
clickTimerRef.current = setTimeout(() => {
|
||||||
|
if (tabValue === 0 && !selectedArticleId && !tempArticleId) {
|
||||||
|
runPreviewFetch(articleId);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleListItemDoubleClick = (articleId: number) => {
|
||||||
|
// Cancel pending single-click preview and proceed to select
|
||||||
|
if (clickTimerRef.current) {
|
||||||
|
clearTimeout(clickTimerRef.current);
|
||||||
|
(clickTimerRef as any).current = null;
|
||||||
|
}
|
||||||
|
handleArticleSelect(articleId);
|
||||||
|
};
|
||||||
|
|
||||||
const previewData = {
|
const previewData = {
|
||||||
heading: currentArticleData[modalLanguage].heading || "",
|
heading: currentArticleData[modalLanguage].heading || "",
|
||||||
@@ -656,11 +674,10 @@ export const ArticleSelectOrCreateDialog = observer(
|
|||||||
filteredArticles.map((article) => (
|
filteredArticles.map((article) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
key={article.id}
|
key={article.id}
|
||||||
onClick={() => handleArticleSelect(article.id)}
|
onClick={() => handleListItemClick(article.id)}
|
||||||
onMouseEnter={() =>
|
onDoubleClick={() =>
|
||||||
setHoveredArticleId(article.id.toString())
|
handleListItemDoubleClick(article.id)
|
||||||
}
|
}
|
||||||
onMouseLeave={() => setHoveredArticleId(null)}
|
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
mb: 0.5,
|
mb: 0.5,
|
||||||
|
|||||||
@@ -340,55 +340,63 @@ class CreateSightStore {
|
|||||||
|
|
||||||
createLeftArticle = async () => {
|
createLeftArticle = async () => {
|
||||||
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
/* ... your existing logic to create a new left article (placeholder or DB) ... */
|
||||||
|
const ruName = (this.sight.ru.name || "").trim();
|
||||||
|
const enName = (this.sight.en.name || "").trim();
|
||||||
|
const zhName = (this.sight.zh.name || "").trim();
|
||||||
|
|
||||||
|
// If all names are empty, skip defaulting and use empty headings
|
||||||
|
const hasAnyName = !!(ruName || enName || zhName);
|
||||||
|
|
||||||
const response = await languageInstance("ru").post("/article", {
|
const response = await languageInstance("ru").post("/article", {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
});
|
});
|
||||||
const newLeftArticleId = response.data.id;
|
const newLeftArticleId = response.data.id;
|
||||||
|
|
||||||
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("en").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
});
|
});
|
||||||
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
await languageInstance("zh").patch(`/article/${newLeftArticleId}`, {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
this.sight.left_article = newLeftArticleId; // Store the actual ID
|
||||||
this.sight.ru.left = {
|
this.sight.ru.left = {
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.en.left = {
|
this.sight.en.left = {
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
this.sight.zh.left = {
|
this.sight.zh.left = {
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
media: [],
|
media: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
articlesStore.articles.ru.push({
|
articlesStore.articles.ru.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "Новая левая статья",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "Заполните контентом",
|
body: "",
|
||||||
service_name: "Новая левая статья",
|
service_name: hasAnyName ? ruName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.en.push({
|
articlesStore.articles.en.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "New Left Article",
|
heading: hasAnyName ? enName : "",
|
||||||
body: "Fill with content",
|
body: "",
|
||||||
service_name: "New Left Article",
|
service_name: hasAnyName ? enName : "",
|
||||||
});
|
});
|
||||||
articlesStore.articles.zh.push({
|
articlesStore.articles.zh.push({
|
||||||
id: newLeftArticleId,
|
id: newLeftArticleId,
|
||||||
heading: "新的左侧文章",
|
heading: hasAnyName ? zhName : "",
|
||||||
body: "填写内容",
|
body: "",
|
||||||
service_name: "新的左侧文章",
|
service_name: hasAnyName ? zhName : "",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newLeftArticleId;
|
return newLeftArticleId;
|
||||||
|
|||||||
@@ -400,16 +400,36 @@ class EditSightStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
createLeftArticle = async () => {
|
createLeftArticle = async () => {
|
||||||
|
const ruName = (this.sight.ru.name || "").trim();
|
||||||
|
const enName = (this.sight.en.name || "").trim();
|
||||||
|
const zhName = (this.sight.zh.name || "").trim();
|
||||||
|
const hasAnyName = !!(ruName || enName || zhName);
|
||||||
|
|
||||||
const response = await languageInstance("ru").post(`/article`, {
|
const response = await languageInstance("ru").post(`/article`, {
|
||||||
heading: "",
|
heading: hasAnyName ? ruName : "",
|
||||||
body: "",
|
body: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sight.common.left_article = response.data.id;
|
this.sight.common.left_article = response.data.id;
|
||||||
|
|
||||||
this.sight.ru.left.heading = "";
|
await languageInstance("en").patch(
|
||||||
this.sight.en.left.heading = "";
|
`/article/${this.sight.common.left_article}`,
|
||||||
this.sight.zh.left.heading = "";
|
{
|
||||||
|
heading: hasAnyName ? enName : "",
|
||||||
|
body: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await languageInstance("zh").patch(
|
||||||
|
`/article/${this.sight.common.left_article}`,
|
||||||
|
{
|
||||||
|
heading: hasAnyName ? zhName : "",
|
||||||
|
body: "",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.sight.ru.left.heading = hasAnyName ? ruName : "";
|
||||||
|
this.sight.en.left.heading = hasAnyName ? enName : "";
|
||||||
|
this.sight.zh.left.heading = hasAnyName ? zhName : "";
|
||||||
this.sight.ru.left.body = "";
|
this.sight.ru.left.body = "";
|
||||||
this.sight.en.left.body = "";
|
this.sight.en.left.body = "";
|
||||||
this.sight.zh.left.body = "";
|
this.sight.zh.left.body = "";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useState, DragEvent, useEffect } from "react";
|
import React, { useRef, DragEvent } from "react";
|
||||||
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
import { Paper, Box, Typography, Button, Tooltip } from "@mui/material";
|
||||||
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
|
import { X, Info, Plus } from "lucide-react"; // Assuming lucide-react for icons
|
||||||
import { editSightStore } from "@shared";
|
import { editSightStore } from "@shared";
|
||||||
@@ -27,18 +27,9 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
tooltipText,
|
tooltipText,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
const { setFileToUpload } = editSightStore;
|
const { setFileToUpload } = editSightStore;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDragOver) {
|
|
||||||
console.log("isDragOver");
|
|
||||||
}
|
|
||||||
}, [isDragOver]);
|
|
||||||
|
|
||||||
// --- Click to select file ---
|
|
||||||
const handleZoneClick = () => {
|
const handleZoneClick = () => {
|
||||||
// Trigger the hidden file input click
|
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,19 +59,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
|||||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault(); // Crucial to allow a drop
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
const handleDrop = async (event: DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault(); // Crucial to allow a drop
|
event.preventDefault(); // Crucial to allow a drop
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
const files = event.dataTransfer.files;
|
const files = event.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function MediaViewer({
|
|||||||
// Используем новый cache manager для очистки кеша
|
// Используем новый cache manager для очистки кеша
|
||||||
clearMediaTransitionCache(
|
clearMediaTransitionCache(
|
||||||
previousMediaId,
|
previousMediaId,
|
||||||
media?.id || null,
|
|
||||||
media?.media_type
|
media?.media_type
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user