720 lines
23 KiB
TypeScript
720 lines
23 KiB
TypeScript
import {
|
||
getAvailableElementTypes,
|
||
getElementDefinition,
|
||
} from '@/entitiy/element/model/interface'
|
||
import { useToast } from '@/lib/hooks/useToast'
|
||
import { generateDefaultLayout } from '@/lib/utils'
|
||
import {
|
||
CellTarget,
|
||
ElementType,
|
||
FormLayoutSettings,
|
||
Template,
|
||
TemplateElement,
|
||
} from '@/type/template'
|
||
import {
|
||
AlertTriangle,
|
||
Eye,
|
||
HelpCircle,
|
||
Plus,
|
||
Settings2,
|
||
Tag,
|
||
Target,
|
||
Trash2,
|
||
Type,
|
||
} from 'lucide-react'
|
||
import React, { useState } from 'react'
|
||
import { Button } from '../ui/button'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'
|
||
import { Checkbox } from '../ui/checkbox'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '../ui/dialog'
|
||
import { Input } from '../ui/input'
|
||
import { Label } from '../ui/label'
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '../ui/select'
|
||
import { Separator } from '../ui/separator'
|
||
import { ElementFormulaBar } from './ElementFormulaBar'
|
||
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||
|
||
interface ElementFormProps {
|
||
formData: Partial<TemplateElement>
|
||
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
|
||
}
|
||
|
||
const CellTargetEditor: React.FC<{
|
||
targets: CellTarget[]
|
||
onChange: (targets: CellTarget[]) => void
|
||
}> = ({ targets, onChange }) => {
|
||
const { warning } = useToast()
|
||
|
||
const handleAddTarget = () => {
|
||
onChange([...targets, { sheet: 'L', cell: '' }])
|
||
}
|
||
|
||
const handleUpdateTarget = (
|
||
index: number,
|
||
field: keyof CellTarget,
|
||
value: string
|
||
) => {
|
||
const updatedTargets = targets.map((target, i) =>
|
||
i === index ? { ...target, [field]: value } : target
|
||
)
|
||
onChange(updatedTargets)
|
||
}
|
||
|
||
const handleRemoveTarget = (index: number) => {
|
||
onChange(targets.filter((_, i) => i !== index))
|
||
}
|
||
|
||
const handleCellInputChange = (index: number, value: string) => {
|
||
// Проверяем на наличие кириллицы
|
||
const hasCyrillic = /[а-яё]/i.test(value)
|
||
|
||
if (hasCyrillic) {
|
||
warning('Кириллические символы не разрешены в адресе ячейки', {
|
||
title: 'Недопустимый символ',
|
||
duration: 3000,
|
||
})
|
||
return // Не обновляем значение
|
||
}
|
||
|
||
handleUpdateTarget(index, 'cell', value)
|
||
}
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Target className="h-4 w-4 text-blue-600" />
|
||
<CardTitle className="text-sm">Целевые ячейки</CardTitle>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleAddTarget}
|
||
className="h-6 px-2 text-xs"
|
||
>
|
||
<Plus className="mr-1 h-3 w-3" />
|
||
Добавить
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<div className="max-h-48 space-y-2 overflow-y-auto">
|
||
{targets.map((target, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex items-center gap-2 rounded-lg border bg-gray-50/50 p-2"
|
||
>
|
||
{/* Кнопки выбора листа */}
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant={target.sheet === 'L' ? 'default' : 'outline'}
|
||
size="sm"
|
||
onClick={() => handleUpdateTarget(index, 'sheet', 'L')}
|
||
className="h-6 w-7 p-0 text-xs"
|
||
>
|
||
L
|
||
</Button>
|
||
<Button
|
||
variant={target.sheet === 'R' ? 'default' : 'outline'}
|
||
size="sm"
|
||
onClick={() => handleUpdateTarget(index, 'sheet', 'R')}
|
||
className="h-6 w-7 p-0 text-xs"
|
||
>
|
||
R
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Поле ввода ячейки */}
|
||
<Input
|
||
placeholder="A12"
|
||
value={target.cell}
|
||
onChange={e => handleCellInputChange(index, e.target.value)}
|
||
className="h-6 w-16 px-2 text-xs"
|
||
/>
|
||
|
||
{/* Кнопка удаления */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleRemoveTarget(index)}
|
||
className="ml-auto h-6 w-6 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
|
||
{targets.length === 0 && (
|
||
<div className="py-4 text-center">
|
||
<Target className="mx-auto mb-1 h-6 w-6 text-gray-300" />
|
||
<p className="text-xs text-gray-500">
|
||
Добавьте ячейки для связи с элементом
|
||
</p>
|
||
<p className="mt-1 text-xs text-gray-400">
|
||
Например: A1, B5, C10
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{targets.length > 0 && (
|
||
<div className="mt-2 flex items-center gap-1 text-xs text-gray-500">
|
||
<HelpCircle className="h-3 w-3" />
|
||
Всего ячеек: {targets.length}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
// Live Preview компонент
|
||
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||
element,
|
||
}) => {
|
||
const elementDefinition = element.type
|
||
? getElementDefinition(element.type)
|
||
: null
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Eye className="h-4 w-4 text-green-600" />
|
||
<CardTitle className="text-sm">Предпросмотр элемента</CardTitle>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<div className="min-h-[200px] rounded-lg border border-gray-200 bg-white p-4">
|
||
{elementDefinition && elementDefinition.Preview ? (
|
||
<elementDefinition.Preview config={element as any} />
|
||
) : (
|
||
<div className="space-y-3">
|
||
{element.label && (
|
||
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||
{element.label}
|
||
{element.required && (
|
||
<span className="ml-1 text-red-500">*</span>
|
||
)}
|
||
</label>
|
||
)}
|
||
|
||
<div className="flex items-center justify-center gap-2 rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
{element.type
|
||
? `Элемент типа "${element.type}" не найден`
|
||
: 'Выберите тип элемента'}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
||
const elementDefinition = formData.type
|
||
? getElementDefinition(formData.type)
|
||
: null
|
||
|
||
return (
|
||
<div className="grid h-full grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{/* Настройки элемента */}
|
||
<div className="max-h-[60vh] space-y-4 overflow-y-auto pr-2">
|
||
{/* Основные настройки */}
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Settings2 className="h-4 w-4 text-blue-600" />
|
||
<CardTitle className="text-sm">Основные настройки</CardTitle>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||
<Type className="h-3 w-3" />
|
||
Тип элемента
|
||
</Label>
|
||
<Select
|
||
value={formData.type}
|
||
onValueChange={value => {
|
||
const newType = value as ElementType
|
||
const elementDefinition = getElementDefinition(newType)
|
||
|
||
setFormData(prev => {
|
||
const updates: Partial<TemplateElement> = {
|
||
type: newType,
|
||
targetCells: [],
|
||
}
|
||
|
||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||
Object.assign(updates, elementDefinition.defaultConfig)
|
||
}
|
||
|
||
return { ...prev, ...updates }
|
||
})
|
||
}}
|
||
>
|
||
<SelectTrigger className="h-9">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{getAvailableElementTypes().map(({ type, label, icon }) => (
|
||
<SelectItem key={type} value={type}>
|
||
<div className="flex items-center gap-2">
|
||
{icon}
|
||
{label}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||
<Tag className="h-3 w-3" />
|
||
Подпись
|
||
</Label>
|
||
<Input
|
||
placeholder="Название поля"
|
||
value={formData.label}
|
||
onChange={e =>
|
||
setFormData(prev => ({ ...prev, label: e.target.value }))
|
||
}
|
||
className="h-9"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label className="flex items-center gap-2 text-sm font-medium">
|
||
<HelpCircle className="h-3 w-3" />
|
||
Подсказка
|
||
</Label>
|
||
<Input
|
||
placeholder="Введите значение..."
|
||
value={formData.placeholder}
|
||
onChange={e =>
|
||
setFormData(prev => ({
|
||
...prev,
|
||
placeholder: e.target.value,
|
||
}))
|
||
}
|
||
className="h-9"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2 py-1">
|
||
<Checkbox
|
||
id="required"
|
||
checked={formData.required}
|
||
onCheckedChange={checked =>
|
||
setFormData(prev => ({ ...prev, required: !!checked }))
|
||
}
|
||
/>
|
||
<Label htmlFor="required" className="text-sm font-medium">
|
||
Обязательное поле
|
||
</Label>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Целевые ячейки */}
|
||
<CellTargetEditor
|
||
targets={formData.targetCells || []}
|
||
onChange={targets =>
|
||
setFormData(prev => ({ ...prev, targetCells: targets }))
|
||
}
|
||
/>
|
||
|
||
{/* Специфичный редактор элемента */}
|
||
{elementDefinition && elementDefinition.Editor && (
|
||
<Card>
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center gap-2">
|
||
<Settings2 className="h-4 w-4 text-purple-600" />
|
||
<CardTitle className="text-sm">
|
||
Дополнительные настройки
|
||
</CardTitle>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<elementDefinition.Editor
|
||
config={formData as any}
|
||
onChange={newConfig =>
|
||
setFormData(prev => ({ ...prev, ...newConfig }))
|
||
}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
|
||
{/* Предпросмотр */}
|
||
<div className="h-fit lg:sticky lg:top-0">
|
||
<ElementPreview element={formData} />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface CanvasSettingsProps {
|
||
settings: FormLayoutSettings
|
||
onChange: (settings: FormLayoutSettings) => void
|
||
}
|
||
|
||
const CanvasSettings: React.FC<CanvasSettingsProps> = ({
|
||
settings,
|
||
onChange,
|
||
}) => {
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">Размер сетки (px)</label>
|
||
<Select
|
||
value={settings.gridSize.toString()}
|
||
onValueChange={value =>
|
||
onChange({ ...settings, gridSize: parseInt(value) })
|
||
}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="10">10px</SelectItem>
|
||
<SelectItem value="20">20px</SelectItem>
|
||
<SelectItem value="25">25px</SelectItem>
|
||
<SelectItem value="50">50px</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2">
|
||
<Checkbox
|
||
id="showGrid"
|
||
checked={settings.showGrid}
|
||
onCheckedChange={checked =>
|
||
onChange({ ...settings, showGrid: !!checked })
|
||
}
|
||
/>
|
||
<label htmlFor="showGrid" className="text-sm font-medium">
|
||
Показывать сетку
|
||
</label>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface ElementConstructorProps {
|
||
template: Template
|
||
onElementAdd: (element: TemplateElement) => void
|
||
// Принимаем частичное обновление элемента, чтобы корректно обновлять только изменённые поля
|
||
onElementUpdate: (
|
||
elementId: string,
|
||
updates: Partial<TemplateElement>
|
||
) => 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> = ({
|
||
template,
|
||
onElementAdd,
|
||
onElementUpdate,
|
||
onElementDelete,
|
||
disabled = false,
|
||
hasUnsavedChanges = false,
|
||
isSaving = false,
|
||
lastSaveError = null,
|
||
onManualSave,
|
||
onClearError,
|
||
onLayoutSettingsChange,
|
||
}) => {
|
||
const elements = template.elements
|
||
const layoutSettings = template.layoutSettings || {
|
||
gridSize: 20,
|
||
showGrid: true,
|
||
}
|
||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
||
null
|
||
)
|
||
// Функция для создания дефолтной конфигурации
|
||
const createDefaultFormData = (type: ElementType = 'text') => {
|
||
const elementDefinition = getElementDefinition(type)
|
||
|
||
const baseData = {
|
||
type,
|
||
name: '',
|
||
label: '',
|
||
targetCells: [],
|
||
placeholder: '',
|
||
required: false,
|
||
options: [],
|
||
}
|
||
|
||
// Применяем defaultConfig если он есть
|
||
if (elementDefinition && elementDefinition.defaultConfig) {
|
||
Object.assign(baseData, elementDefinition.defaultConfig)
|
||
}
|
||
|
||
return baseData
|
||
}
|
||
|
||
const [formData, setFormData] = useState<Partial<TemplateElement>>(
|
||
createDefaultFormData()
|
||
)
|
||
|
||
const resetForm = () => {
|
||
setFormData(createDefaultFormData())
|
||
}
|
||
|
||
const handleAddElement = () => {
|
||
if (!formData.label) {
|
||
return
|
||
}
|
||
|
||
// Получаем определение элемента
|
||
const elementDefinition = getElementDefinition(formData.type as ElementType)
|
||
if (!elementDefinition) {
|
||
console.error(`Element definition for ${formData.type} not found`)
|
||
return
|
||
}
|
||
|
||
// Генерируем layout для нового элемента
|
||
const defaultLayout = generateDefaultLayout(elements.length)
|
||
|
||
// Используем targetCells из формы или генерируем через mapToCells
|
||
let targetCells = formData.targetCells || []
|
||
if (!targetCells.length) {
|
||
targetCells = elementDefinition.mapToCells(formData as any) || []
|
||
}
|
||
|
||
const newElement: TemplateElement = {
|
||
...formData, // Копируем все поля из formData, включая специфичные для элемента
|
||
id: Date.now().toString(),
|
||
type: formData.type as ElementType,
|
||
name: formData.label || '',
|
||
label: formData.label,
|
||
targetCells,
|
||
placeholder: formData.placeholder,
|
||
required: formData.required,
|
||
options: formData.options,
|
||
order: elements.length,
|
||
layout: defaultLayout,
|
||
} as TemplateElement
|
||
|
||
onElementAdd(newElement)
|
||
resetForm()
|
||
setIsAddDialogOpen(false)
|
||
}
|
||
|
||
const handleEditElement = (element: TemplateElement) => {
|
||
setEditingElement(element)
|
||
setFormData(element)
|
||
}
|
||
|
||
const handleUpdateElement = () => {
|
||
if (!editingElement || !formData.label) return
|
||
|
||
// Получаем определение элемента
|
||
const elementDefinition = getElementDefinition(
|
||
editingElement.type as ElementType
|
||
)
|
||
if (!elementDefinition) {
|
||
console.error(`Element definition for ${editingElement.type} not found`)
|
||
return
|
||
}
|
||
|
||
// Используем targetCells из формы или генерируем через mapToCells
|
||
let targetCells = formData.targetCells || []
|
||
if (!targetCells.length) {
|
||
targetCells = elementDefinition.mapToCells(formData as any) || []
|
||
}
|
||
|
||
const updatedData = {
|
||
...formData,
|
||
name: formData.label || formData.name || '',
|
||
targetCells,
|
||
}
|
||
|
||
onElementUpdate(editingElement.id, updatedData)
|
||
setEditingElement(null)
|
||
resetForm()
|
||
}
|
||
|
||
const handleCancelEdit = () => {
|
||
setEditingElement(null)
|
||
resetForm()
|
||
}
|
||
|
||
const handleDeleteAndSelect = (elementId: string) => {
|
||
onElementDelete(elementId)
|
||
setEditingElement(null)
|
||
resetForm()
|
||
}
|
||
|
||
const handleElementCopy = (element: Omit<TemplateElement, 'id'>) => {
|
||
const newElement: TemplateElement = {
|
||
...element,
|
||
id: Date.now().toString(),
|
||
order: elements.length,
|
||
}
|
||
onElementAdd(newElement)
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-full w-full flex-col bg-background">
|
||
<ElementFormulaBar
|
||
elementCount={elements.length}
|
||
onAddElement={() => setIsAddDialogOpen(true)}
|
||
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}
|
||
onElementAdd={handleElementCopy}
|
||
/>
|
||
</div>
|
||
|
||
{/* Диалог добавления элемента */}
|
||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
||
<DialogHeader className="pb-4">
|
||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||
<Plus className="h-5 w-5 text-blue-600" />
|
||
Добавить новый элемент
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
Создайте и настройте новый элемент формы. Вы можете увидеть
|
||
предпросмотр справа.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="flex-1 overflow-hidden">
|
||
<ElementForm formData={formData} setFormData={setFormData} />
|
||
</div>
|
||
<Separator className="my-4" />
|
||
<div className="flex items-center justify-between pt-2">
|
||
<div className="text-sm text-gray-500">
|
||
{!formData.label && (
|
||
<div className="flex items-center gap-1 text-amber-600">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
Заполните обязательные поля
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button
|
||
onClick={() => setIsAddDialogOpen(false)}
|
||
variant="outline"
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
onClick={handleAddElement}
|
||
disabled={disabled || !formData.label}
|
||
className="min-w-[140px]"
|
||
>
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
Добавить элемент
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
{/* Диалог редактирования элемента */}
|
||
{editingElement && (
|
||
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
|
||
<DialogContent className="max-h-[90vh] max-w-6xl overflow-hidden">
|
||
<DialogHeader className="pb-4">
|
||
<DialogTitle className="flex items-center gap-2 text-lg">
|
||
<Settings2 className="h-5 w-5 text-green-600" />
|
||
Редактировать элемент
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
Измените параметры элемента "
|
||
{editingElement.label || editingElement.name}".
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="flex-1 overflow-hidden">
|
||
<ElementForm formData={formData} setFormData={setFormData} />
|
||
</div>
|
||
<Separator className="my-4" />
|
||
<div className="flex items-center justify-between pt-2">
|
||
<div className="text-sm text-gray-500">
|
||
{!formData.label && (
|
||
<div className="flex items-center gap-1 text-amber-600">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
Заполните обязательные поля
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<Button onClick={handleCancelEdit} variant="outline">
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
onClick={handleUpdateElement}
|
||
disabled={disabled || !formData.label}
|
||
className="min-w-[160px]"
|
||
>
|
||
<Settings2 className="mr-2 h-4 w-4" />
|
||
Сохранить изменения
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|