elements constructor улучшен
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { Save } from 'lucide-react'
|
||||
import { memo } from 'react'
|
||||
import { FormulaBarProps } from '../types'
|
||||
import { getColumnLabel } from '../utils'
|
||||
@@ -163,19 +164,7 @@ export const FormulaBar = memo(
|
||||
: 'Все изменения сохранены'
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<Save className="h-3 w-3" />
|
||||
{lastSaveError
|
||||
? 'Повторить'
|
||||
: hasUnsavedChanges
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '../ui/dialog'
|
||||
import { Input } from '../ui/input'
|
||||
import {
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
SelectValue,
|
||||
} from '../ui/select'
|
||||
import { Textarea } from '../ui/textarea'
|
||||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||
|
||||
interface ElementFormProps {
|
||||
@@ -705,6 +705,14 @@ interface ElementConstructorProps {
|
||||
) => void
|
||||
onElementDelete: (elementId: string) => void
|
||||
disabled?: boolean
|
||||
// Пропсы для сохранения
|
||||
hasUnsavedChanges?: boolean
|
||||
isSaving?: boolean
|
||||
lastSaveError?: string | null
|
||||
onManualSave?: () => Promise<void>
|
||||
onClearError?: () => void
|
||||
// Пропсы для настроек макета
|
||||
onLayoutSettingsChange?: (settings: FormLayoutSettings) => void
|
||||
}
|
||||
|
||||
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
@@ -713,6 +721,12 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
onElementUpdate,
|
||||
onElementDelete,
|
||||
disabled = false,
|
||||
hasUnsavedChanges = false,
|
||||
isSaving = false,
|
||||
lastSaveError = null,
|
||||
onManualSave,
|
||||
onClearError,
|
||||
onLayoutSettingsChange,
|
||||
}) => {
|
||||
const elements = template.elements
|
||||
const layoutSettings = template.layoutSettings || {
|
||||
@@ -877,24 +891,44 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
{/* Main Content */}
|
||||
<div className="flex flex-1 flex-col bg-gray-50">
|
||||
<div className="flex items-center justify-between border-b bg-white p-2">
|
||||
<div className="px-2">
|
||||
<h3 className="text-sm font-medium">Холст</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Перетаскивайте элементы для настройки их расположения
|
||||
</p>
|
||||
<div className="flex h-full w-full flex-col bg-background">
|
||||
<ElementFormulaBar
|
||||
elementCount={elements.length}
|
||||
onAddElement={() => 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
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<VisualLayoutEditor
|
||||
elements={elements}
|
||||
layoutSettings={layoutSettings}
|
||||
onElementUpdate={onElementUpdate}
|
||||
onElementDelete={handleDeleteAndSelect}
|
||||
onLayoutSettingsChange={onLayoutSettingsChange || (() => {})}
|
||||
onElementEdit={handleEditElement}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
{/* Диалог добавления элемента */}
|
||||
<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">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||
@@ -904,10 +938,7 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
</DialogHeader>
|
||||
<ElementForm formData={formData} setFormData={setFormData} />
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
onClick={() => setIsAddDialogOpen(false)}
|
||||
variant="outline"
|
||||
>
|
||||
<Button onClick={() => setIsAddDialogOpen(false)} variant="outline">
|
||||
Отмена
|
||||
</Button>
|
||||
<Button
|
||||
@@ -928,11 +959,9 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Диалог редактирования элемента */}
|
||||
{editingElement && (
|
||||
<Dialog
|
||||
open={!!editingElement}
|
||||
onOpenChange={() => handleCancelEdit()}
|
||||
>
|
||||
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
|
||||
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||
@@ -953,18 +982,5 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<VisualLayoutEditor
|
||||
elements={elements}
|
||||
layoutSettings={layoutSettings}
|
||||
onElementUpdate={onElementUpdate}
|
||||
onElementDelete={handleDeleteAndSelect}
|
||||
onLayoutSettingsChange={() => {}} // Настройки макета только для чтения
|
||||
onElementEdit={handleEditElement}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
190
src/component/TemplateManager/ElementFormulaBar.tsx
Normal file
190
src/component/TemplateManager/ElementFormulaBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
)
|
||||
128
src/component/TemplateManager/SaveButton.tsx
Normal file
128
src/component/TemplateManager/SaveButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -4,8 +4,6 @@ import {
|
||||
Droplets,
|
||||
Edit,
|
||||
Edit3,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Gauge,
|
||||
Grid,
|
||||
Move,
|
||||
@@ -460,21 +458,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
||||
}, [layoutSettings, onLayoutSettingsChange])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gray-50">
|
||||
<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 className="flex h-full flex-col bg-background">
|
||||
<div
|
||||
className="relative flex-1 overflow-auto"
|
||||
onClick={handleCanvasClick}
|
||||
|
||||
111
src/hook/useDelayedSave.ts
Normal file
111
src/hook/useDelayedSave.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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<Template | null>(
|
||||
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)
|
||||
}
|
||||
updateData(updatedTemplate)
|
||||
}
|
||||
|
||||
const handleElementUpdate = async (
|
||||
@@ -63,8 +72,6 @@ export const ElementsCreation: FC = () => {
|
||||
) => {
|
||||
if (!selectedTemplate) return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
const updatedTemplate = {
|
||||
...selectedTemplate,
|
||||
elements: selectedTemplate.elements.map(el =>
|
||||
@@ -81,60 +88,70 @@ export const ElementsCreation: FC = () => {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
await updateTemplate(updatedTemplate)
|
||||
setSelectedTemplate(updatedTemplate)
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении элемента:', error)
|
||||
alert('Ошибка при обновлении элемента. Попробуйте еще раз.')
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
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)
|
||||
}
|
||||
updateData(updatedTemplate)
|
||||
}
|
||||
|
||||
// Компактные уведомления вместо полноэкранных блокеров
|
||||
if (isLoading || error || !selectedTemplate) {
|
||||
// if (isLoading) {
|
||||
// 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 (
|
||||
<div className="p-4">
|
||||
<div
|
||||
className={`rounded-md border px-4 py-2 text-sm ${
|
||||
error
|
||||
? 'border-destructive/50 bg-destructive/10 text-destructive'
|
||||
: !selectedTemplate
|
||||
? 'border-destructive/50 bg-destructive/10 text-destructive'
|
||||
: 'border-muted bg-muted/20 text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{isLoading && 'Загрузка шаблона...'}
|
||||
{error && `Ошибка загрузки: ${error}`}
|
||||
{!selectedTemplate && 'Шаблон не найден'}
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h3 className="mb-2 text-lg font-medium text-destructive">
|
||||
Ошибка загрузки
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-muted-foreground">{error.message}</p>
|
||||
<Button onClick={() => window.location.reload()}>
|
||||
Обновить страницу
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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 gap-3">
|
||||
<Button
|
||||
@@ -178,22 +195,22 @@ export const ElementsCreation: FC = () => {
|
||||
</Button>
|
||||
</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 className="flex-1 overflow-hidden">
|
||||
<ElementConstructor
|
||||
template={selectedTemplate}
|
||||
onElementAdd={handleElementAdd}
|
||||
onElementUpdate={handleElementUpdate}
|
||||
onElementDelete={handleElementDelete}
|
||||
disabled={isSaving}
|
||||
disabled={false}
|
||||
hasUnsavedChanges={hasUnsavedChanges}
|
||||
isSaving={isSaving}
|
||||
lastSaveError={lastSaveError}
|
||||
onManualSave={manualSave}
|
||||
onClearError={clearError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user