-
-
-
Холст
-
- Перетаскивайте элементы для настройки их расположения
-
-
-
-
+
+
setIsAddDialogOpen(true)}
+ onAutoArrange={handleAutoArrange}
+ disabled={disabled}
+ hasUnsavedChanges={hasUnsavedChanges}
+ isSaving={isSaving}
+ lastSaveError={lastSaveError}
+ onManualSave={onManualSave}
+ onClearError={onClearError}
+ showGrid={layoutSettings.showGrid}
+ onToggleGrid={
+ onLayoutSettingsChange
+ ? () => {
+ const newSettings = {
+ ...layoutSettings,
+ showGrid: !layoutSettings.showGrid,
+ }
+ onLayoutSettingsChange(newSettings)
+ }
+ : undefined
+ }
+ />
- {editingElement && (
-
- )}
-
-
-
- {}} // Настройки макета только для чтения
- onElementEdit={handleEditElement}
- />
-
+
+ {})}
+ onElementEdit={handleEditElement}
+ />
+
+ {/* Диалог добавления элемента */}
+
+
+ {/* Диалог редактирования элемента */}
+ {editingElement && (
+
+ )}
)
}
diff --git a/src/component/TemplateManager/ElementFormulaBar.tsx b/src/component/TemplateManager/ElementFormulaBar.tsx
new file mode 100644
index 0000000..d5c2b4d
--- /dev/null
+++ b/src/component/TemplateManager/ElementFormulaBar.tsx
@@ -0,0 +1,190 @@
+import { Eye, EyeOff, Plus, Save, Wrench } from 'lucide-react'
+import { memo } from 'react'
+import { Button } from '../ui/button'
+
+interface ElementFormulaBarProps {
+ elementCount: number
+ onAddElement: () => void
+ onAutoArrange?: () => void
+ disabled?: boolean
+ // Пропсы для кнопки сохранения
+ hasUnsavedChanges?: boolean
+ isSaving?: boolean
+ lastSaveError?: string | null
+ onManualSave?: () => Promise
+ onClearError?: () => void
+ // Пропсы для управления сеткой
+ showGrid?: boolean
+ onToggleGrid?: () => void
+}
+
+export const ElementFormulaBar = memo(
+ ({
+ elementCount,
+ onAddElement,
+ onAutoArrange,
+ disabled = false,
+ hasUnsavedChanges = false,
+ isSaving = false,
+ lastSaveError = null,
+ onManualSave,
+ onClearError,
+ showGrid = true,
+ onToggleGrid,
+ }: ElementFormulaBarProps) => {
+ return (
+
+
+
+
+
+ {elementCount} элементов
+
+
+
+
+ {onAutoArrange && (
+
+ )}
+ {onToggleGrid && (
+
+ )}
+
+
+
+
+ {/* Ошибка сохранения */}
+ {lastSaveError && (
+
+
+
+ Ошибка сохранения
+
+ {onClearError && (
+
+ )}
+
+ )}
+
+ {/* Индикатор сохранения */}
+ {isSaving && (
+
+
+
Сохранение...
+
+ )}
+
+ {/* Кнопка сохранения */}
+ {onManualSave && (
+
+ )}
+
+
+
+ )
+ }
+)
diff --git a/src/component/TemplateManager/SaveButton.tsx b/src/component/TemplateManager/SaveButton.tsx
new file mode 100644
index 0000000..50b6d25
--- /dev/null
+++ b/src/component/TemplateManager/SaveButton.tsx
@@ -0,0 +1,128 @@
+import { memo } from 'react'
+
+interface SaveButtonProps {
+ hasUnsavedChanges: boolean
+ isSaving: boolean
+ lastSaveError: string | null
+ onManualSave: () => Promise
+ onClearError: () => void
+}
+
+export const SaveButton = memo(
+ ({
+ hasUnsavedChanges,
+ isSaving,
+ lastSaveError,
+ onManualSave,
+ onClearError,
+ }: SaveButtonProps) => {
+ return (
+
+ {/* Ошибка сохранения */}
+ {lastSaveError && (
+
+
+
+ Ошибка сохранения
+
+
+
+ )}
+
+ {/* Индикатор сохранения */}
+ {isSaving && (
+
+
+
Сохранение...
+
+ )}
+
+ {/* Кнопка сохранения */}
+
+
+ )
+ }
+)
diff --git a/src/component/TemplateManager/VisualLayoutEditor.tsx b/src/component/TemplateManager/VisualLayoutEditor.tsx
index bb82103..34c1719 100644
--- a/src/component/TemplateManager/VisualLayoutEditor.tsx
+++ b/src/component/TemplateManager/VisualLayoutEditor.tsx
@@ -4,8 +4,6 @@ import {
Droplets,
Edit,
Edit3,
- Eye,
- EyeOff,
Gauge,
Grid,
Move,
@@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC = ({
}, [layoutSettings, onLayoutSettingsChange])
return (
-
-
-
- {elements.length} элементов на холсте
-
-
-
-
+
{
+ onSave: (data: T) => Promise
+ onSuccess?: (data: T) => void // Коллбек после успешного сохранения
+ delay?: number // Задержка в миллисекундах (по умолчанию 2000мс)
+}
+
+interface UseDelayedSaveReturn {
+ hasUnsavedChanges: boolean
+ isSaving: boolean
+ lastSaveError: string | null
+ updateData: (data: T) => void
+ manualSave: () => Promise
+ clearError: () => void
+}
+
+export function useDelayedSave({
+ onSave,
+ onSuccess,
+ delay = 2000,
+}: UseDelayedSaveOptions): UseDelayedSaveReturn {
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
+ const [lastSaveError, setLastSaveError] = useState(null)
+
+ const timeoutRef = useRef(null)
+ const pendingDataRef = useRef(null)
+
+ // Очистка таймера при размонтировании компонента
+ useEffect(() => {
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+ }
+ }, [])
+
+ const clearError = useCallback(() => {
+ setLastSaveError(null)
+ }, [])
+
+ const performSave = useCallback(
+ async (data: T) => {
+ if (isSaving) return
+
+ setIsSaving(true)
+ setLastSaveError(null)
+
+ try {
+ await onSave(data)
+ setHasUnsavedChanges(false)
+ pendingDataRef.current = null
+ // Вызываем коллбек успешного сохранения
+ onSuccess?.(data)
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Неизвестная ошибка'
+ setLastSaveError(errorMessage)
+ console.error('Ошибка при сохранении:', error)
+ } finally {
+ setIsSaving(false)
+ }
+ },
+ [onSave, onSuccess, isSaving]
+ )
+
+ const updateData = useCallback(
+ (data: T) => {
+ // Отменяем предыдущий таймер
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ }
+
+ // Сохраняем данные для отложенного сохранения
+ pendingDataRef.current = data
+ setHasUnsavedChanges(true)
+ setLastSaveError(null)
+
+ // Устанавливаем новый таймер
+ timeoutRef.current = setTimeout(() => {
+ if (pendingDataRef.current) {
+ performSave(pendingDataRef.current)
+ }
+ }, delay)
+ },
+ [delay, performSave]
+ )
+
+ const manualSave = useCallback(async () => {
+ // Отменяем автоматическое сохранение
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current)
+ timeoutRef.current = null
+ }
+
+ // Сохраняем немедленно, если есть несохранённые данные
+ if (pendingDataRef.current) {
+ await performSave(pendingDataRef.current)
+ }
+ }, [performSave])
+
+ return {
+ hasUnsavedChanges,
+ isSaving,
+ lastSaveError,
+ updateData,
+ manualSave,
+ clearError,
+ }
+}
diff --git a/src/page/ElementsCreation/Page.tsx b/src/page/ElementsCreation/Page.tsx
index a9f438c..e49c30b 100644
--- a/src/page/ElementsCreation/Page.tsx
+++ b/src/page/ElementsCreation/Page.tsx
@@ -1,9 +1,10 @@
import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor'
import { Button } from '@/component/ui/button'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
+import { useDelayedSave } from '@/hook/useDelayedSave'
import { Template, TemplateElement } from '@/type/template'
import { ArrowLeft, FileText, Wrench } from 'lucide-react'
-import { FC, useEffect, useState } from 'react'
+import { FC, useCallback, useEffect, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
export const ElementsCreation: FC = () => {
@@ -13,48 +14,56 @@ export const ElementsCreation: FC = () => {
const [selectedTemplate, setSelectedTemplate] = useState(
null
)
- const [isSaving, setIsSaving] = useState(false)
+ const [isInitialized, setIsInitialized] = useState(false)
// Автоматически выбираем шаблон, если передан templateId
useEffect(() => {
- if (templateId) {
+ if (templateId && !isInitialized) {
const template = templates.find(t => t.id === templateId)
if (template) {
setSelectedTemplate(template)
+ setIsInitialized(true)
} else if (!isLoading && templates.length > 0) {
// Если шаблон не найден после загрузки, перенаправляем на страницу со списком шаблонов
navigate('/templates', { replace: true })
}
}
- }, [templateId, templates, navigate, isLoading])
+ }, [templateId, templates, navigate, isLoading, isInitialized])
+
+ // Функция для сохранения шаблона в БД
+ const saveTemplate = useCallback(
+ async (template: Template) => {
+ await updateTemplate(template)
+ },
+ [updateTemplate]
+ )
+
+ // Хук для отложенного сохранения
+ const {
+ hasUnsavedChanges,
+ isSaving,
+ lastSaveError,
+ updateData,
+ manualSave,
+ clearError,
+ } = useDelayedSave({
+ onSave: saveTemplate,
+ delay: 2000, // 2 секунды задержки
+ })
const handleElementAdd = async (element: TemplateElement) => {
- // console.log('handleElementAdd called with element:', element)
-
if (!selectedTemplate) {
- // console.log('No selected template')
return
}
- setIsSaving(true)
- try {
- const updatedTemplate = {
- ...selectedTemplate,
- elements: [...selectedTemplate.elements, element],
- updatedAt: new Date(),
- }
-
- // console.log('Updated template:', updatedTemplate)
- // console.log('Elements count:', updatedTemplate.elements.length)
-
- await updateTemplate(updatedTemplate)
- setSelectedTemplate(updatedTemplate)
- } catch (error) {
- console.error('Ошибка при добавлении элемента:', error)
- alert('Ошибка при добавлении элемента. Попробуйте еще раз.')
- } finally {
- setIsSaving(false)
+ const updatedTemplate = {
+ ...selectedTemplate,
+ elements: [...selectedTemplate.elements, element],
+ updatedAt: new Date(),
}
+
+ setSelectedTemplate(updatedTemplate)
+ updateData(updatedTemplate)
}
const handleElementUpdate = async (
@@ -63,137 +72,145 @@ export const ElementsCreation: FC = () => {
) => {
if (!selectedTemplate) return
- setIsSaving(true)
- try {
- const updatedTemplate = {
- ...selectedTemplate,
- elements: selectedTemplate.elements.map(el =>
- el.id === elementId
- ? {
- ...el,
- // Обновляем только определённые ключи, чтобы не затирать существующие значения `undefined`
- ...Object.fromEntries(
- Object.entries(updates).filter(([, v]) => v !== undefined)
- ),
- }
- : el
- ),
- updatedAt: new Date(),
- }
-
- await updateTemplate(updatedTemplate)
- setSelectedTemplate(updatedTemplate)
- } catch (error) {
- console.error('Ошибка при обновлении элемента:', error)
- alert('Ошибка при обновлении элемента. Попробуйте еще раз.')
- } finally {
- setIsSaving(false)
+ const updatedTemplate = {
+ ...selectedTemplate,
+ elements: selectedTemplate.elements.map(el =>
+ el.id === elementId
+ ? {
+ ...el,
+ // Обновляем только определённые ключи, чтобы не затирать существующие значения `undefined`
+ ...Object.fromEntries(
+ Object.entries(updates).filter(([, v]) => v !== undefined)
+ ),
+ }
+ : el
+ ),
+ updatedAt: new Date(),
}
+
+ setSelectedTemplate(updatedTemplate)
+ updateData(updatedTemplate)
}
const handleElementDelete = async (elementId: string) => {
if (!selectedTemplate) return
- setIsSaving(true)
- try {
- const updatedTemplate = {
- ...selectedTemplate,
- elements: selectedTemplate.elements.filter(el => el.id !== elementId),
- updatedAt: new Date(),
- }
-
- await updateTemplate(updatedTemplate)
- setSelectedTemplate(updatedTemplate)
- } catch (error) {
- console.error('Ошибка при удалении элемента:', error)
- alert('Ошибка при удалении элемента. Попробуйте еще раз.')
- } finally {
- setIsSaving(false)
+ const updatedTemplate = {
+ ...selectedTemplate,
+ elements: selectedTemplate.elements.filter(el => el.id !== elementId),
+ updatedAt: new Date(),
}
+
+ setSelectedTemplate(updatedTemplate)
+ updateData(updatedTemplate)
}
- // Компактные уведомления вместо полноэкранных блокеров
- if (isLoading || error || !selectedTemplate) {
+ // if (isLoading) {
+ // return (
+ //
+ //
+ //
+ //
Загрузка шаблонов...
+ //
+ //
+ // )
+ // }
+
+ if (error) {
return (
-
-
- {isLoading && 'Загрузка шаблона...'}
- {error && `Ошибка загрузки: ${error}`}
- {!selectedTemplate && 'Шаблон не найден'}
+
+
+
+ Ошибка загрузки
+
+
{error.message}
+
+
+
+ )
+ }
+
+ if (!selectedTemplate) {
+ return (
+
+
+
+
Шаблон не найден
+
+ Перейдите к списку шаблонов для выбора
+
+
)
}
return (
-
-
-
-
-
-
-
Редактор интерфейса
-
—
-
- {selectedTemplate.name}
-
+
+
+
+
+
+
+
+
Редактор интерфейса
+ —
+
+ {selectedTemplate.name}
+
+
+
+
+
+
-
-
-
-
- {/* Индикатор сохранения */}
- {isSaving && (
-
- )}
-
-
+
+
+
)
}