elements constructor улучшен

This commit is contained in:
2025-07-22 10:33:21 +03:00
parent ac8be1a347
commit 553da77a87
7 changed files with 688 additions and 253 deletions

View File

@@ -1,3 +1,4 @@
import { Save } from 'lucide-react'
import { memo } from 'react' import { memo } from 'react'
import { FormulaBarProps } from '../types' import { FormulaBarProps } from '../types'
import { getColumnLabel } from '../utils' import { getColumnLabel } from '../utils'
@@ -163,19 +164,7 @@ export const FormulaBar = memo(
: 'Все изменения сохранены' : 'Все изменения сохранены'
} }
> >
<svg <Save className="h-3 w-3" />
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{lastSaveError {lastSaveError
? 'Повторить' ? 'Повторить'
: hasUnsavedChanges : hasUnsavedChanges

View File

@@ -34,7 +34,6 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger,
} from '../ui/dialog' } from '../ui/dialog'
import { Input } from '../ui/input' import { Input } from '../ui/input'
import { import {
@@ -45,6 +44,7 @@ import {
SelectValue, SelectValue,
} from '../ui/select' } from '../ui/select'
import { Textarea } from '../ui/textarea' import { Textarea } from '../ui/textarea'
import { ElementFormulaBar } from './ElementFormulaBar'
import { VisualLayoutEditor } from './VisualLayoutEditor' import { VisualLayoutEditor } from './VisualLayoutEditor'
interface ElementFormProps { interface ElementFormProps {
@@ -705,6 +705,14 @@ interface ElementConstructorProps {
) => void ) => void
onElementDelete: (elementId: string) => void onElementDelete: (elementId: string) => void
disabled?: boolean disabled?: boolean
// Пропсы для сохранения
hasUnsavedChanges?: boolean
isSaving?: boolean
lastSaveError?: string | null
onManualSave?: () => Promise<void>
onClearError?: () => void
// Пропсы для настроек макета
onLayoutSettingsChange?: (settings: FormLayoutSettings) => void
} }
export const ElementConstructor: React.FC<ElementConstructorProps> = ({ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
@@ -713,6 +721,12 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
onElementUpdate, onElementUpdate,
onElementDelete, onElementDelete,
disabled = false, disabled = false,
hasUnsavedChanges = false,
isSaving = false,
lastSaveError = null,
onManualSave,
onClearError,
onLayoutSettingsChange,
}) => { }) => {
const elements = template.elements const elements = template.elements
const layoutSettings = template.layoutSettings || { const layoutSettings = template.layoutSettings || {
@@ -877,24 +891,44 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
// }; // };
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full flex-col bg-background">
{/* Main Content */} <ElementFormulaBar
<div className="flex flex-1 flex-col bg-gray-50"> elementCount={elements.length}
<div className="flex items-center justify-between border-b bg-white p-2"> onAddElement={() => setIsAddDialogOpen(true)}
<div className="px-2"> onAutoArrange={handleAutoArrange}
<h3 className="text-sm font-medium">Холст</h3> disabled={disabled}
<p className="text-xs text-gray-500"> hasUnsavedChanges={hasUnsavedChanges}
Перетаскивайте элементы для настройки их расположения isSaving={isSaving}
</p> lastSaveError={lastSaveError}
onManualSave={onManualSave}
onClearError={onClearError}
showGrid={layoutSettings.showGrid}
onToggleGrid={
onLayoutSettingsChange
? () => {
const newSettings = {
...layoutSettings,
showGrid: !layoutSettings.showGrid,
}
onLayoutSettingsChange(newSettings)
}
: undefined
}
/>
<div className="flex-1 overflow-hidden">
<VisualLayoutEditor
elements={elements}
layoutSettings={layoutSettings}
onElementUpdate={onElementUpdate}
onElementDelete={handleDeleteAndSelect}
onLayoutSettingsChange={onLayoutSettingsChange || (() => {})}
onElementEdit={handleEditElement}
/>
</div> </div>
<div className="flex items-center gap-2">
{/* Диалог добавления элемента */}
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" disabled={disabled}>
<Plus className="mr-2 h-4 w-4" />
Добавить элемент
</Button>
</DialogTrigger>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto"> <DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Добавить новый элемент</DialogTitle> <DialogTitle>Добавить новый элемент</DialogTitle>
@@ -904,10 +938,7 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</DialogHeader> </DialogHeader>
<ElementForm formData={formData} setFormData={setFormData} /> <ElementForm formData={formData} setFormData={setFormData} />
<div className="flex justify-end gap-2 pt-4"> <div className="flex justify-end gap-2 pt-4">
<Button <Button onClick={() => setIsAddDialogOpen(false)} variant="outline">
onClick={() => setIsAddDialogOpen(false)}
variant="outline"
>
Отмена Отмена
</Button> </Button>
<Button <Button
@@ -928,11 +959,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Диалог редактирования элемента */}
{editingElement && ( {editingElement && (
<Dialog <Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
open={!!editingElement}
onOpenChange={() => handleCancelEdit()}
>
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto"> <DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Редактировать элемент</DialogTitle> <DialogTitle>Редактировать элемент</DialogTitle>
@@ -953,18 +982,5 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
</Dialog> </Dialog>
)} )}
</div> </div>
</div>
<div className="flex-1 overflow-hidden">
<VisualLayoutEditor
elements={elements}
layoutSettings={layoutSettings}
onElementUpdate={onElementUpdate}
onElementDelete={handleDeleteAndSelect}
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
onElementEdit={handleEditElement}
/>
</div>
</div>
</div>
) )
} }

View File

@@ -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<void>
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 (
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="flex min-w-[100px] items-center justify-center rounded-md border bg-muted px-2 py-1">
<span className="text-xs font-medium text-muted-foreground">
{elementCount} элементов
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="outline"
size="sm"
onClick={onAddElement}
disabled={disabled}
className="h-7 px-2 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
Добавить
</Button>
{onAutoArrange && (
<Button
variant="outline"
size="sm"
onClick={onAutoArrange}
disabled={disabled || elementCount === 0}
className="h-7 px-2 text-xs"
>
<Wrench className="mr-1 h-3 w-3" />
Упорядочить
</Button>
)}
{onToggleGrid && (
<Button
variant="outline"
size="sm"
onClick={onToggleGrid}
className="h-7 px-2 text-xs"
>
{showGrid ? (
<EyeOff className="mr-1 h-3 w-3" />
) : (
<Eye className="mr-1 h-3 w-3" />
)}
{showGrid ? 'Скрыть сетку' : 'Показать'}
</Button>
)}
</div>
</div>
<div className="flex items-center gap-2">
{/* Ошибка сохранения */}
{lastSaveError && (
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
<svg
className="h-3 w-3 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<span
className="text-xs text-destructive"
title={lastSaveError}
>
Ошибка сохранения
</span>
{onClearError && (
<button
onClick={onClearError}
className="ml-1 text-destructive/60 hover:text-destructive"
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
)}
{/* Индикатор сохранения */}
{isSaving && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-3.5 w-3.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="ml-1 text-xs">Сохранение...</span>
</div>
)}
{/* Кнопка сохранения */}
{onManualSave && (
<button
onClick={onManualSave}
disabled={isSaving}
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground'
} disabled:opacity-50`}
title={
lastSaveError
? 'Повторить сохранение'
: hasUnsavedChanges
? 'Есть несохраненные изменения'
: 'Все изменения сохранены'
}
>
<Save className="h-3 w-3" />
{lastSaveError
? 'Повторить'
: hasUnsavedChanges
? 'Сохранить'
: 'Сохранено'}
</button>
)}
</div>
</div>
</div>
)
}
)

View File

@@ -0,0 +1,128 @@
import { memo } from 'react'
interface SaveButtonProps {
hasUnsavedChanges: boolean
isSaving: boolean
lastSaveError: string | null
onManualSave: () => Promise<void>
onClearError: () => void
}
export const SaveButton = memo(
({
hasUnsavedChanges,
isSaving,
lastSaveError,
onManualSave,
onClearError,
}: SaveButtonProps) => {
return (
<div className="flex items-center gap-2">
{/* Ошибка сохранения */}
{lastSaveError && (
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
<svg
className="h-3 w-3 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<span className="text-xs text-destructive" title={lastSaveError}>
Ошибка сохранения
</span>
<button
onClick={onClearError}
className="ml-1 text-destructive/60 hover:text-destructive"
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
)}
{/* Индикатор сохранения */}
{isSaving && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-3.5 w-3.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="ml-1 text-xs">Сохранение...</span>
</div>
)}
{/* Кнопка сохранения */}
<button
onClick={onManualSave}
disabled={isSaving}
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground'
} disabled:opacity-50`}
title={
lastSaveError
? 'Повторить сохранение'
: hasUnsavedChanges
? 'Есть несохраненные изменения'
: 'Все изменения сохранены'
}
>
<svg
className="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
{lastSaveError
? 'Повторить'
: hasUnsavedChanges
? 'Сохранить'
: 'Сохранено'}
</button>
</div>
)
}
)

View File

@@ -4,8 +4,6 @@ import {
Droplets, Droplets,
Edit, Edit,
Edit3, Edit3,
Eye,
EyeOff,
Gauge, Gauge,
Grid, Grid,
Move, Move,
@@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
}, [layoutSettings, onLayoutSettingsChange]) }, [layoutSettings, onLayoutSettingsChange])
return ( return (
<div className="flex h-full flex-col bg-gray-50"> <div className="flex h-full flex-col bg-background">
<div className="flex shrink-0 items-center justify-between border-b bg-white p-2">
<span className="px-2 text-sm text-gray-600">
{elements.length} элементов на холсте
</span>
<Button variant="outline" size="sm" onClick={toggleGrid}>
{layoutSettings.showGrid ? (
<EyeOff className="mr-2 h-4 w-4" />
) : (
<Eye className="mr-2 h-4 w-4" />
)}
{layoutSettings.showGrid ? 'Скрыть сетку' : 'Показать сетку'}
</Button>
</div>
<div <div
className="relative flex-1 overflow-auto" className="relative flex-1 overflow-auto"
onClick={handleCanvasClick} onClick={handleCanvasClick}

111
src/hook/useDelayedSave.ts Normal file
View File

@@ -0,0 +1,111 @@
import { useCallback, useEffect, useRef, useState } from 'react'
interface UseDelayedSaveOptions<T> {
onSave: (data: T) => Promise<void>
onSuccess?: (data: T) => void // Коллбек после успешного сохранения
delay?: number // Задержка в миллисекундах (по умолчанию 2000мс)
}
interface UseDelayedSaveReturn<T> {
hasUnsavedChanges: boolean
isSaving: boolean
lastSaveError: string | null
updateData: (data: T) => void
manualSave: () => Promise<void>
clearError: () => void
}
export function useDelayedSave<T>({
onSave,
onSuccess,
delay = 2000,
}: UseDelayedSaveOptions<T>): UseDelayedSaveReturn<T> {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [lastSaveError, setLastSaveError] = useState<string | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const pendingDataRef = useRef<T | null>(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,
}
}

View File

@@ -1,9 +1,10 @@
import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor' import { ElementConstructor } from '@/component/TemplateManager/ElementConstructor'
import { Button } from '@/component/ui/button' import { Button } from '@/component/ui/button'
import { useTemplateContext } from '@/entitiy/template/model/TemplateContext' import { useTemplateContext } from '@/entitiy/template/model/TemplateContext'
import { useDelayedSave } from '@/hook/useDelayedSave'
import { Template, TemplateElement } from '@/type/template' import { Template, TemplateElement } from '@/type/template'
import { ArrowLeft, FileText, Wrench } from 'lucide-react' 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' import { useNavigate, useParams } from 'react-router-dom'
export const ElementsCreation: FC = () => { export const ElementsCreation: FC = () => {
@@ -13,48 +14,56 @@ export const ElementsCreation: FC = () => {
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>( const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
null null
) )
const [isSaving, setIsSaving] = useState(false) const [isInitialized, setIsInitialized] = useState(false)
// Автоматически выбираем шаблон, если передан templateId // Автоматически выбираем шаблон, если передан templateId
useEffect(() => { useEffect(() => {
if (templateId) { if (templateId && !isInitialized) {
const template = templates.find(t => t.id === templateId) const template = templates.find(t => t.id === templateId)
if (template) { if (template) {
setSelectedTemplate(template) setSelectedTemplate(template)
setIsInitialized(true)
} else if (!isLoading && templates.length > 0) { } else if (!isLoading && templates.length > 0) {
// Если шаблон не найден после загрузки, перенаправляем на страницу со списком шаблонов // Если шаблон не найден после загрузки, перенаправляем на страницу со списком шаблонов
navigate('/templates', { replace: true }) 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) => { const handleElementAdd = async (element: TemplateElement) => {
// console.log('handleElementAdd called with element:', element)
if (!selectedTemplate) { if (!selectedTemplate) {
// console.log('No selected template')
return return
} }
setIsSaving(true)
try {
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: [...selectedTemplate.elements, element], elements: [...selectedTemplate.elements, element],
updatedAt: new Date(), updatedAt: new Date(),
} }
// console.log('Updated template:', updatedTemplate)
// console.log('Elements count:', updatedTemplate.elements.length)
await updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate) setSelectedTemplate(updatedTemplate)
} catch (error) { updateData(updatedTemplate)
console.error('Ошибка при добавлении элемента:', error)
alert('Ошибка при добавлении элемента. Попробуйте еще раз.')
} finally {
setIsSaving(false)
}
} }
const handleElementUpdate = async ( const handleElementUpdate = async (
@@ -63,8 +72,6 @@ export const ElementsCreation: FC = () => {
) => { ) => {
if (!selectedTemplate) return if (!selectedTemplate) return
setIsSaving(true)
try {
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: selectedTemplate.elements.map(el => elements: selectedTemplate.elements.map(el =>
@@ -81,60 +88,70 @@ export const ElementsCreation: FC = () => {
updatedAt: new Date(), updatedAt: new Date(),
} }
await updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate) setSelectedTemplate(updatedTemplate)
} catch (error) { updateData(updatedTemplate)
console.error('Ошибка при обновлении элемента:', error)
alert('Ошибка при обновлении элемента. Попробуйте еще раз.')
} finally {
setIsSaving(false)
}
} }
const handleElementDelete = async (elementId: string) => { const handleElementDelete = async (elementId: string) => {
if (!selectedTemplate) return if (!selectedTemplate) return
setIsSaving(true)
try {
const updatedTemplate = { const updatedTemplate = {
...selectedTemplate, ...selectedTemplate,
elements: selectedTemplate.elements.filter(el => el.id !== elementId), elements: selectedTemplate.elements.filter(el => el.id !== elementId),
updatedAt: new Date(), updatedAt: new Date(),
} }
await updateTemplate(updatedTemplate)
setSelectedTemplate(updatedTemplate) setSelectedTemplate(updatedTemplate)
} catch (error) { updateData(updatedTemplate)
console.error('Ошибка при удалении элемента:', error)
alert('Ошибка при удалении элемента. Попробуйте еще раз.')
} finally {
setIsSaving(false)
}
} }
// Компактные уведомления вместо полноэкранных блокеров // if (isLoading) {
if (isLoading || error || !selectedTemplate) { // return (
// <div className="flex h-screen items-center justify-center">
// <div className="text-center">
// <div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
// <p className="text-sm text-muted-foreground">Загрузка шаблонов...</p>
// </div>
// </div>
// )
// }
if (error) {
return ( return (
<div className="p-4"> <div className="flex h-screen items-center justify-center">
<div <div className="text-center">
className={`rounded-md border px-4 py-2 text-sm ${ <h3 className="mb-2 text-lg font-medium text-destructive">
error Ошибка загрузки
? 'border-destructive/50 bg-destructive/10 text-destructive' </h3>
: !selectedTemplate <p className="mb-4 text-sm text-muted-foreground">{error.message}</p>
? 'border-destructive/50 bg-destructive/10 text-destructive' <Button onClick={() => window.location.reload()}>
: 'border-muted bg-muted/20 text-muted-foreground' Обновить страницу
}`} </Button>
> </div>
{isLoading && 'Загрузка шаблона...'} </div>
{error && `Ошибка загрузки: ${error}`} )
{!selectedTemplate && 'Шаблон не найден'} }
if (!selectedTemplate) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<Wrench className="mx-auto mb-4 h-16 w-16 text-muted-foreground" />
<h3 className="mb-2 text-lg font-medium">Шаблон не найден</h3>
<p className="mb-4 text-sm text-muted-foreground">
Перейдите к списку шаблонов для выбора
</p>
<Button onClick={() => navigate('/templates')}>
К списку шаблонов
</Button>
</div> </div>
</div> </div>
) )
} }
return ( return (
<div className="flex-shrink-0 border-b bg-card px-4 py-1"> <div className="flex h-screen flex-col">
<div className="flex-shrink-0 border-b bg-card px-4 py-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
@@ -178,22 +195,22 @@ export const ElementsCreation: FC = () => {
</Button> </Button>
</div> </div>
</div> </div>
{/* Индикатор сохранения */}
{isSaving && (
<div className="ml-auto flex items-center space-x-2 text-sm text-muted-foreground">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
<span>Сохранение...</span>
</div> </div>
)}
<div className="flex-1 overflow-hidden">
<ElementConstructor <ElementConstructor
template={selectedTemplate} template={selectedTemplate}
onElementAdd={handleElementAdd} onElementAdd={handleElementAdd}
onElementUpdate={handleElementUpdate} onElementUpdate={handleElementUpdate}
onElementDelete={handleElementDelete} onElementDelete={handleElementDelete}
disabled={isSaving} disabled={false}
hasUnsavedChanges={hasUnsavedChanges}
isSaving={isSaving}
lastSaveError={lastSaveError}
onManualSave={manualSave}
onClearError={clearError}
/> />
</div> </div>
</div>
) )
} }