сетка точками и выделение
This commit is contained in:
@@ -1,58 +1,91 @@
|
|||||||
import { Calendar, LayoutGrid, Plus, Settings, Trash2 } from 'lucide-react';
|
import { Calendar, LayoutGrid, Plus, Settings, Trash2 } from 'lucide-react'
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react'
|
||||||
import { getAvailableElementTypes, getElementDefinition } from '../../lib/element-registry';
|
import {
|
||||||
import { autoArrangeElements, findFreePosition, generateDefaultLayout } from '../../lib/utils';
|
getAvailableElementTypes,
|
||||||
import { CellTarget, ElementType, FormLayoutSettings, TemplateElement } from '../../types/template';
|
getElementDefinition,
|
||||||
import { Button } from '../ui/button';
|
} from '../../lib/element-registry'
|
||||||
import { Checkbox } from '../ui/checkbox';
|
import {
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
|
autoArrangeElements,
|
||||||
import { Input } from '../ui/input';
|
findFreePosition,
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
generateDefaultLayout,
|
||||||
import { Textarea } from '../ui/textarea';
|
} from '../../lib/utils'
|
||||||
import { VisualLayoutEditor } from './VisualLayoutEditor';
|
import {
|
||||||
|
CellTarget,
|
||||||
|
ElementType,
|
||||||
|
FormLayoutSettings,
|
||||||
|
TemplateElement,
|
||||||
|
} from '../../types/template'
|
||||||
|
import { Button } from '../ui/button'
|
||||||
|
import { Checkbox } from '../ui/checkbox'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '../ui/dialog'
|
||||||
|
import { Input } from '../ui/input'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '../ui/select'
|
||||||
|
import { Textarea } from '../ui/textarea'
|
||||||
|
import { VisualLayoutEditor } from './VisualLayoutEditor'
|
||||||
|
|
||||||
interface ElementFormProps {
|
interface ElementFormProps {
|
||||||
formData: Partial<TemplateElement>;
|
formData: Partial<TemplateElement>
|
||||||
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>;
|
setFormData: React.Dispatch<React.SetStateAction<Partial<TemplateElement>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const CellTargetEditor: React.FC<{
|
const CellTargetEditor: React.FC<{
|
||||||
targets: CellTarget[];
|
targets: CellTarget[]
|
||||||
onChange: (targets: CellTarget[]) => void;
|
onChange: (targets: CellTarget[]) => void
|
||||||
}> = ({ targets, onChange }) => {
|
}> = ({ targets, onChange }) => {
|
||||||
const handleAddTarget = () => {
|
const handleAddTarget = () => {
|
||||||
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }]);
|
onChange([...targets, { sheet: 'L', cell: '', displayName: '' }])
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleUpdateTarget = (index: number, field: keyof CellTarget, value: string) => {
|
const handleUpdateTarget = (
|
||||||
|
index: number,
|
||||||
|
field: keyof CellTarget,
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
const updatedTargets = targets.map((target, i) =>
|
const updatedTargets = targets.map((target, i) =>
|
||||||
i === index ? { ...target, [field]: value } : target
|
i === index ? { ...target, [field]: value } : target,
|
||||||
);
|
)
|
||||||
onChange(updatedTargets);
|
onChange(updatedTargets)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRemoveTarget = (index: number) => {
|
const handleRemoveTarget = (index: number) => {
|
||||||
onChange(targets.filter((_, i) => i !== index));
|
onChange(targets.filter((_, i) => i !== index))
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium">Целевые ячейки</label>
|
<label className="text-sm font-medium">Целевые ячейки</label>
|
||||||
<Button variant="outline" size="sm" onClick={handleAddTarget}>
|
<Button variant="outline" size="sm" onClick={handleAddTarget}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
Добавить ячейку
|
Добавить ячейку
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 max-h-48 overflow-y-auto">
|
<div className="max-h-48 space-y-3 overflow-y-auto">
|
||||||
{targets.map((target, index) => (
|
{targets.map((target, index) => (
|
||||||
<div key={index} className="p-3 border rounded-lg bg-gray-50">
|
<div key={index} className="rounded-lg border bg-gray-50 p-3">
|
||||||
<div className="grid grid-cols-3 gap-2 mb-2">
|
<div className="mb-2 grid grid-cols-3 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-medium text-gray-600">Лист</label>
|
<label className="text-xs font-medium text-gray-600">
|
||||||
|
Лист
|
||||||
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={target.sheet}
|
value={target.sheet}
|
||||||
onValueChange={(value) => handleUpdateTarget(index, 'sheet', value)}
|
onValueChange={(value) =>
|
||||||
|
handleUpdateTarget(index, 'sheet', value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -66,20 +99,28 @@ const CellTargetEditor: React.FC<{
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-medium text-gray-600">Ячейка</label>
|
<label className="text-xs font-medium text-gray-600">
|
||||||
|
Ячейка
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="A1, B5..."
|
placeholder="A1, B5..."
|
||||||
value={target.cell}
|
value={target.cell}
|
||||||
onChange={(e) => handleUpdateTarget(index, 'cell', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleUpdateTarget(index, 'cell', e.target.value)
|
||||||
|
}
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-medium text-gray-600">Название</label>
|
<label className="text-xs font-medium text-gray-600">
|
||||||
|
Название
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Описание"
|
placeholder="Описание"
|
||||||
value={target.displayName || ''}
|
value={target.displayName || ''}
|
||||||
onChange={(e) => handleUpdateTarget(index, 'displayName', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleUpdateTarget(index, 'displayName', e.target.value)
|
||||||
|
}
|
||||||
className="h-8"
|
className="h-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,27 +131,31 @@ const CellTargetEditor: React.FC<{
|
|||||||
onClick={() => handleRemoveTarget(index)}
|
onClick={() => handleRemoveTarget(index)}
|
||||||
className="h-6 text-xs text-red-600 hover:text-red-700"
|
className="h-6 text-xs text-red-600 hover:text-red-700"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-3 w-3 mr-1" />
|
<Trash2 className="mr-1 h-3 w-3" />
|
||||||
Удалить
|
Удалить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{targets.length === 0 && (
|
{targets.length === 0 && (
|
||||||
<p className="text-sm text-gray-500 text-center py-4">
|
<p className="py-4 text-center text-sm text-gray-500">
|
||||||
Добавьте ячейки для связи с элементом
|
Добавьте ячейки для связи с элементом
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Live Preview компонент
|
// Live Preview компонент
|
||||||
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ element }) => {
|
const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({
|
||||||
const elementDefinition = element.type ? getElementDefinition(element.type) : null;
|
element,
|
||||||
|
}) => {
|
||||||
|
const elementDefinition = element.type
|
||||||
|
? getElementDefinition(element.type)
|
||||||
|
: null
|
||||||
|
|
||||||
if (elementDefinition && elementDefinition.Preview) {
|
if (elementDefinition && elementDefinition.Preview) {
|
||||||
return <elementDefinition.Preview config={element as any} />;
|
return <elementDefinition.Preview config={element as any} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback для старых элементов
|
// Fallback для старых элементов
|
||||||
@@ -123,7 +168,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
disabled
|
disabled
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
return (
|
return (
|
||||||
@@ -133,7 +178,7 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
rows={3}
|
rows={3}
|
||||||
className="w-full resize-none"
|
className="w-full resize-none"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'number':
|
case 'number':
|
||||||
return (
|
return (
|
||||||
@@ -143,16 +188,10 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
disabled
|
disabled
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'date':
|
case 'date':
|
||||||
return (
|
return <Input type="date" disabled className="w-full" />
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
disabled
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
@@ -163,39 +202,41 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
</span>
|
</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'radio':
|
case 'radio':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{element.options?.length ? element.options.map((option, index) => (
|
{element.options?.length ? (
|
||||||
|
element.options.map((option, index) => (
|
||||||
<div key={index} className="flex items-center space-x-2">
|
<div key={index} className="flex items-center space-x-2">
|
||||||
<div className="w-4 h-4 border border-gray-300 rounded-full" />
|
<div className="h-4 w-4 rounded-full border border-gray-300" />
|
||||||
<span className="text-sm">{option.label}</span>
|
<span className="text-sm">{option.label}</span>
|
||||||
</div>
|
</div>
|
||||||
)) : (
|
))
|
||||||
|
) : (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-4 h-4 border border-gray-300 rounded-full" />
|
<div className="h-4 w-4 rounded-full border border-gray-300" />
|
||||||
<span className="text-sm text-gray-500">Вариант 1</span>
|
<span className="text-sm text-gray-500">Вариант 1</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'checkbox':
|
case 'checkbox':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="w-4 h-4 border border-gray-300 rounded" />
|
<div className="h-4 w-4 rounded border border-gray-300" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{element.placeholder || element.label || 'Отметить'}
|
{element.placeholder || element.label || 'Отметить'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
case 'standards':
|
case 'standards':
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between p-3 border border-input rounded-md bg-background">
|
<div className="flex items-center justify-between rounded-md border border-input bg-background p-3">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{element.placeholder || 'Выберите эталоны'}
|
{element.placeholder || 'Выберите эталоны'}
|
||||||
</span>
|
</span>
|
||||||
@@ -208,34 +249,34 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
Выбрано эталонов: 0
|
Выбрано эталонов: 0
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="p-3 border border-gray-200 rounded bg-gray-50 text-center text-sm text-gray-500">
|
<div className="rounded border border-gray-200 bg-gray-50 p-3 text-center text-sm text-gray-500">
|
||||||
Выберите тип элемента
|
Выберите тип элемента
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
<div className="text-sm font-medium text-gray-700">Предпросмотр</div>
|
||||||
<div className="p-4 border border-gray-200 rounded-lg bg-white">
|
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
{element.label && (
|
{element.label && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="mb-2 block text-sm font-medium text-gray-700">
|
||||||
{element.label}
|
{element.label}
|
||||||
{element.required && <span className="text-red-500 ml-1">*</span>}
|
{element.required && <span className="ml-1 text-red-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{renderPreviewInput()}
|
{renderPreviewInput()}
|
||||||
{element.targetCells && element.targetCells.length > 0 && (
|
{element.targetCells && element.targetCells.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
{element.targetCells.map((target, index) => (
|
{element.targetCells.map((target, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={index}
|
||||||
className="inline-block px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs rounded"
|
className="inline-block rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
|
||||||
title={target.displayName}
|
title={target.displayName}
|
||||||
>
|
>
|
||||||
{target.sheet}!{target.cell}
|
{target.sheet}!{target.cell}
|
||||||
@@ -245,34 +286,42 @@ const ElementPreview: React.FC<{ element: Partial<TemplateElement> }> = ({ eleme
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
||||||
const elementDefinition = formData.type ? getElementDefinition(formData.type) : null;
|
const elementDefinition = formData.type
|
||||||
|
? getElementDefinition(formData.type)
|
||||||
|
: null
|
||||||
|
|
||||||
const handleAddOption = () => {
|
const handleAddOption = () => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
options: [...(prev.options || []), { value: '', label: '' }],
|
options: [...(prev.options || []), { value: '', label: '' }],
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleUpdateOption = (index: number, field: 'value' | 'label', value: string) => {
|
const handleUpdateOption = (
|
||||||
setFormData(prev => ({
|
index: number,
|
||||||
|
field: 'value' | 'label',
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
options: prev.options?.map((option, i) => (i === index ? { ...option, [field]: value } : option)),
|
options: prev.options?.map((option, i) =>
|
||||||
}));
|
i === index ? { ...option, [field]: value } : option,
|
||||||
};
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const handleRemoveOption = (index: number) => {
|
const handleRemoveOption = (index: number) => {
|
||||||
setFormData(prev => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
options: prev.options?.filter((_, i) => i !== index),
|
options: prev.options?.filter((_, i) => i !== index),
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
const needsOptions = formData.type === 'select' || formData.type === 'radio';
|
const needsOptions = formData.type === 'select' || formData.type === 'radio'
|
||||||
|
|
||||||
// Если есть определение элемента в реестре, используем его редактор
|
// Если есть определение элемента в реестре, используем его редактор
|
||||||
if (elementDefinition && elementDefinition.Editor) {
|
if (elementDefinition && elementDefinition.Editor) {
|
||||||
@@ -284,31 +333,35 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<label className="text-sm font-medium">Тип элемента</label>
|
<label className="text-sm font-medium">Тип элемента</label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.type}
|
value={formData.type}
|
||||||
onValueChange={value => {
|
onValueChange={(value) => {
|
||||||
const newType = value as ElementType;
|
const newType = value as ElementType
|
||||||
console.log('Element type changed to:', newType);
|
console.log('Element type changed to:', newType)
|
||||||
|
|
||||||
setFormData(prev => {
|
setFormData((prev) => {
|
||||||
const updates: Partial<TemplateElement> = { type: newType };
|
const updates: Partial<TemplateElement> = { type: newType }
|
||||||
|
|
||||||
// Если выбран тип "standards", устанавливаем значения по умолчанию
|
// Если выбран тип "standards", устанавливаем значения по умолчанию
|
||||||
if (newType === 'standards') {
|
if (newType === 'standards') {
|
||||||
console.log('Standards type selected, getting element definition');
|
console.log(
|
||||||
const elementDefinition = getElementDefinition('standards');
|
'Standards type selected, getting element definition',
|
||||||
console.log('Element definition:', elementDefinition);
|
)
|
||||||
|
const elementDefinition = getElementDefinition('standards')
|
||||||
|
console.log('Element definition:', elementDefinition)
|
||||||
|
|
||||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||||
const defaultConfig = elementDefinition.defaultConfig as any;
|
const defaultConfig =
|
||||||
console.log('Default config:', defaultConfig);
|
elementDefinition.defaultConfig as any
|
||||||
updates.targetCells = elementDefinition.mapToCells?.(defaultConfig) || [];
|
console.log('Default config:', defaultConfig)
|
||||||
console.log('Generated targetCells:', updates.targetCells);
|
updates.targetCells =
|
||||||
|
elementDefinition.mapToCells?.(defaultConfig) || []
|
||||||
|
console.log('Generated targetCells:', updates.targetCells)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFormData = { ...prev, ...updates };
|
const newFormData = { ...prev, ...updates }
|
||||||
console.log('New form data:', newFormData);
|
console.log('New form data:', newFormData)
|
||||||
return newFormData;
|
return newFormData
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -333,7 +386,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="field_name"
|
placeholder="field_name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -342,20 +397,26 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Название поля"
|
placeholder="Название поля"
|
||||||
value={formData.label}
|
value={formData.label}
|
||||||
onChange={e => setFormData(prev => ({ ...prev, label: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, label: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CellTargetEditor
|
<CellTargetEditor
|
||||||
targets={formData.targetCells || []}
|
targets={formData.targetCells || []}
|
||||||
onChange={(targets) => setFormData(prev => ({ ...prev, targetCells: targets }))}
|
onChange={(targets) =>
|
||||||
|
setFormData((prev) => ({ ...prev, targetCells: targets }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Специфичный редактор элемента */}
|
{/* Специфичный редактор элемента */}
|
||||||
<elementDefinition.Editor
|
<elementDefinition.Editor
|
||||||
config={formData as any}
|
config={formData as any}
|
||||||
onChange={(newConfig) => setFormData(prev => ({ ...prev, ...newConfig }))}
|
onChange={(newConfig) =>
|
||||||
|
setFormData((prev) => ({ ...prev, ...newConfig }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -364,7 +425,7 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<ElementPreview element={formData} />
|
<ElementPreview element={formData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback для старых элементов
|
// Fallback для старых элементов
|
||||||
@@ -376,31 +437,40 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<label className="text-sm font-medium">Тип элемента</label>
|
<label className="text-sm font-medium">Тип элемента</label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.type}
|
value={formData.type}
|
||||||
onValueChange={value => {
|
onValueChange={(value) => {
|
||||||
const newType = value as ElementType;
|
const newType = value as ElementType
|
||||||
console.log('Element type changed to (fallback):', newType);
|
console.log('Element type changed to (fallback):', newType)
|
||||||
|
|
||||||
setFormData(prev => {
|
setFormData((prev) => {
|
||||||
const updates: Partial<TemplateElement> = { type: newType };
|
const updates: Partial<TemplateElement> = { type: newType }
|
||||||
|
|
||||||
// Если выбран тип "standards", устанавливаем значения по умолчанию
|
// Если выбран тип "standards", устанавливаем значения по умолчанию
|
||||||
if (newType === 'standards') {
|
if (newType === 'standards') {
|
||||||
console.log('Standards type selected (fallback), getting element definition');
|
console.log(
|
||||||
const elementDefinition = getElementDefinition('standards');
|
'Standards type selected (fallback), getting element definition',
|
||||||
console.log('Element definition (fallback):', elementDefinition);
|
)
|
||||||
|
const elementDefinition = getElementDefinition('standards')
|
||||||
|
console.log(
|
||||||
|
'Element definition (fallback):',
|
||||||
|
elementDefinition,
|
||||||
|
)
|
||||||
|
|
||||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||||
const defaultConfig = elementDefinition.defaultConfig as any;
|
const defaultConfig = elementDefinition.defaultConfig as any
|
||||||
console.log('Default config (fallback):', defaultConfig);
|
console.log('Default config (fallback):', defaultConfig)
|
||||||
updates.targetCells = elementDefinition.mapToCells?.(defaultConfig) || [];
|
updates.targetCells =
|
||||||
console.log('Generated targetCells (fallback):', updates.targetCells);
|
elementDefinition.mapToCells?.(defaultConfig) || []
|
||||||
|
console.log(
|
||||||
|
'Generated targetCells (fallback):',
|
||||||
|
updates.targetCells,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFormData = { ...prev, ...updates };
|
const newFormData = { ...prev, ...updates }
|
||||||
console.log('New form data (fallback):', newFormData);
|
console.log('New form data (fallback):', newFormData)
|
||||||
return newFormData;
|
return newFormData
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -425,7 +495,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="field_name"
|
placeholder="field_name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -434,7 +506,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Название поля"
|
placeholder="Название поля"
|
||||||
value={formData.label}
|
value={formData.label}
|
||||||
onChange={e => setFormData(prev => ({ ...prev, label: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, label: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -444,7 +518,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Введите значение..."
|
placeholder="Введите значение..."
|
||||||
value={formData.placeholder}
|
value={formData.placeholder}
|
||||||
onChange={e => setFormData(prev => ({ ...prev, placeholder: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setFormData((prev) => ({ ...prev, placeholder: e.target.value }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -452,7 +528,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="required"
|
id="required"
|
||||||
checked={formData.required}
|
checked={formData.required}
|
||||||
onCheckedChange={checked => setFormData(prev => ({ ...prev, required: !!checked }))}
|
onCheckedChange={(checked) =>
|
||||||
|
setFormData((prev) => ({ ...prev, required: !!checked }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="required" className="text-sm font-medium">
|
<label htmlFor="required" className="text-sm font-medium">
|
||||||
Обязательное поле
|
Обязательное поле
|
||||||
@@ -461,7 +539,9 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
|
|
||||||
<CellTargetEditor
|
<CellTargetEditor
|
||||||
targets={formData.targetCells || []}
|
targets={formData.targetCells || []}
|
||||||
onChange={(targets) => setFormData(prev => ({ ...prev, targetCells: targets }))}
|
onChange={(targets) =>
|
||||||
|
setFormData((prev) => ({ ...prev, targetCells: targets }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{needsOptions && (
|
{needsOptions && (
|
||||||
@@ -469,24 +549,32 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium">Варианты</label>
|
<label className="text-sm font-medium">Варианты</label>
|
||||||
<Button variant="outline" size="sm" onClick={handleAddOption}>
|
<Button variant="outline" size="sm" onClick={handleAddOption}>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
Добавить
|
Добавить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 max-h-32 overflow-y-auto">
|
<div className="max-h-32 space-y-2 overflow-y-auto">
|
||||||
{formData.options?.map((option, index) => (
|
{formData.options?.map((option, index) => (
|
||||||
<div key={index} className="flex gap-2">
|
<div key={index} className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Значение"
|
placeholder="Значение"
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onChange={e => handleUpdateOption(index, 'value', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleUpdateOption(index, 'value', e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Подпись"
|
placeholder="Подпись"
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={e => handleUpdateOption(index, 'label', e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleUpdateOption(index, 'label', e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" size="icon" onClick={() => handleRemoveOption(index)}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleRemoveOption(index)}
|
||||||
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,22 +589,27 @@ const ElementForm: React.FC<ElementFormProps> = ({ formData, setFormData }) => {
|
|||||||
<ElementPreview element={formData} />
|
<ElementPreview element={formData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
|
||||||
|
|
||||||
interface CanvasSettingsProps {
|
|
||||||
settings: FormLayoutSettings;
|
|
||||||
onChange: (settings: FormLayoutSettings) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CanvasSettings: React.FC<CanvasSettingsProps> = ({ settings, onChange }) => {
|
interface CanvasSettingsProps {
|
||||||
|
settings: FormLayoutSettings
|
||||||
|
onChange: (settings: FormLayoutSettings) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CanvasSettings: React.FC<CanvasSettingsProps> = ({
|
||||||
|
settings,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Размер сетки (px)</label>
|
<label className="text-sm font-medium">Размер сетки (px)</label>
|
||||||
<Select
|
<Select
|
||||||
value={settings.gridSize.toString()}
|
value={settings.gridSize.toString()}
|
||||||
onValueChange={(value) => onChange({ ...settings, gridSize: parseInt(value) })}
|
onValueChange={(value) =>
|
||||||
|
onChange({ ...settings, gridSize: parseInt(value) })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -534,24 +627,29 @@ const CanvasSettings: React.FC<CanvasSettingsProps> = ({ settings, onChange }) =
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
id="showGrid"
|
id="showGrid"
|
||||||
checked={settings.showGrid}
|
checked={settings.showGrid}
|
||||||
onCheckedChange={(checked) => onChange({ ...settings, showGrid: !!checked })}
|
onCheckedChange={(checked) =>
|
||||||
|
onChange({ ...settings, showGrid: !!checked })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="showGrid" className="text-sm font-medium">
|
<label htmlFor="showGrid" className="text-sm font-medium">
|
||||||
Показывать сетку
|
Показывать сетку
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
interface ElementConstructorProps {
|
interface ElementConstructorProps {
|
||||||
elements: TemplateElement[];
|
elements: TemplateElement[]
|
||||||
layoutSettings: FormLayoutSettings;
|
layoutSettings: FormLayoutSettings
|
||||||
onElementAdd: (element: TemplateElement) => void;
|
onElementAdd: (element: TemplateElement) => void
|
||||||
onElementUpdate: (elementId: string, updates: Partial<TemplateElement>) => void;
|
onElementUpdate: (
|
||||||
onElementDelete: (elementId: string) => void;
|
elementId: string,
|
||||||
onElementsReorder: (elements: TemplateElement[]) => void;
|
updates: Partial<TemplateElement>,
|
||||||
onLayoutSettingsChange: (settings: FormLayoutSettings) => void;
|
) => void
|
||||||
|
onElementDelete: (elementId: string) => void
|
||||||
|
onElementsReorder: (elements: TemplateElement[]) => void
|
||||||
|
onLayoutSettingsChange: (settings: FormLayoutSettings) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
||||||
@@ -563,9 +661,11 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
// onElementsReorder,
|
// onElementsReorder,
|
||||||
onLayoutSettingsChange,
|
onLayoutSettingsChange,
|
||||||
}) => {
|
}) => {
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false)
|
||||||
const [editingElement, setEditingElement] = useState<TemplateElement | null>(null);
|
const [editingElement, setEditingElement] = useState<TemplateElement | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
const [formData, setFormData] = useState<Partial<TemplateElement>>({
|
const [formData, setFormData] = useState<Partial<TemplateElement>>({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: '',
|
name: '',
|
||||||
@@ -574,7 +674,7 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
placeholder: '',
|
placeholder: '',
|
||||||
required: false,
|
required: false,
|
||||||
options: [],
|
options: [],
|
||||||
});
|
})
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -585,42 +685,44 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
placeholder: '',
|
placeholder: '',
|
||||||
required: false,
|
required: false,
|
||||||
options: [],
|
options: [],
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleAddElement = () => {
|
const handleAddElement = () => {
|
||||||
console.log('handleAddElement called with formData:', formData);
|
console.log('handleAddElement called with formData:', formData)
|
||||||
|
|
||||||
if (!formData.name || !formData.label) {
|
if (!formData.name || !formData.label) {
|
||||||
console.log('Validation failed: missing name or label');
|
console.log('Validation failed: missing name or label')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
|
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
|
||||||
if (formData.type !== 'standards' && !formData.targetCells?.length) {
|
if (formData.type !== 'standards' && !formData.targetCells?.length) {
|
||||||
console.log('Validation failed: missing targetCells for non-standards element');
|
console.log(
|
||||||
return;
|
'Validation failed: missing targetCells for non-standards element',
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем layout для нового элемента
|
// Генерируем layout для нового элемента
|
||||||
const existingLayouts = elements.map(e => e.layout).filter(Boolean);
|
const existingLayouts = elements.map((e) => e.layout).filter(Boolean)
|
||||||
const defaultLayout = generateDefaultLayout(elements.length);
|
const defaultLayout = generateDefaultLayout(elements.length)
|
||||||
|
|
||||||
const layout = findFreePosition(
|
const layout = findFreePosition(
|
||||||
defaultLayout,
|
defaultLayout,
|
||||||
existingLayouts as any[],
|
existingLayouts as any[],
|
||||||
layoutSettings.gridSize
|
layoutSettings.gridSize,
|
||||||
);
|
)
|
||||||
|
|
||||||
// Специальная обработка для эталонов
|
// Специальная обработка для эталонов
|
||||||
let targetCells = formData.targetCells || [];
|
let targetCells = formData.targetCells || []
|
||||||
if (formData.type === 'standards') {
|
if (formData.type === 'standards') {
|
||||||
// Для эталонов генерируем targetCells автоматически, если их нет
|
// Для эталонов генерируем targetCells автоматически, если их нет
|
||||||
if (!targetCells.length) {
|
if (!targetCells.length) {
|
||||||
const elementDefinition = getElementDefinition('standards');
|
const elementDefinition = getElementDefinition('standards')
|
||||||
if (elementDefinition && elementDefinition.defaultConfig) {
|
if (elementDefinition && elementDefinition.defaultConfig) {
|
||||||
const defaultConfig = elementDefinition.defaultConfig as any;
|
const defaultConfig = elementDefinition.defaultConfig as any
|
||||||
targetCells = elementDefinition.mapToCells?.(defaultConfig) || [];
|
targetCells = elementDefinition.mapToCells?.(defaultConfig) || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -636,75 +738,76 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
options: formData.options,
|
options: formData.options,
|
||||||
order: elements.length,
|
order: elements.length,
|
||||||
layout,
|
layout,
|
||||||
};
|
}
|
||||||
|
|
||||||
onElementAdd(newElement);
|
onElementAdd(newElement)
|
||||||
resetForm();
|
resetForm()
|
||||||
setIsAddDialogOpen(false);
|
setIsAddDialogOpen(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEditElement = (element: TemplateElement) => {
|
const handleEditElement = (element: TemplateElement) => {
|
||||||
setEditingElement(element);
|
setEditingElement(element)
|
||||||
setFormData(element);
|
setFormData(element)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleUpdateElement = () => {
|
const handleUpdateElement = () => {
|
||||||
if (!editingElement || !formData.name || !formData.label) return;
|
if (!editingElement || !formData.name || !formData.label) return
|
||||||
|
|
||||||
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
|
// Для элемента типа "standards" не требуем targetCells, так как они генерируются автоматически
|
||||||
if (formData.type !== 'standards' && !formData.targetCells?.length) return;
|
if (formData.type !== 'standards' && !formData.targetCells?.length) return
|
||||||
|
|
||||||
onElementUpdate(editingElement.id, formData);
|
onElementUpdate(editingElement.id, formData)
|
||||||
setEditingElement(null);
|
setEditingElement(null)
|
||||||
resetForm();
|
resetForm()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setEditingElement(null);
|
setEditingElement(null)
|
||||||
resetForm();
|
resetForm()
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleAutoArrange = () => {
|
const handleAutoArrange = () => {
|
||||||
const arrangedElements = autoArrangeElements(
|
const arrangedElements = autoArrangeElements(
|
||||||
elements,
|
elements,
|
||||||
layoutSettings.gridSize
|
layoutSettings.gridSize,
|
||||||
);
|
)
|
||||||
|
|
||||||
arrangedElements.forEach((element, index) => {
|
arrangedElements.forEach((element, index) => {
|
||||||
onElementUpdate(element.id, { layout: element.layout, order: index });
|
onElementUpdate(element.id, { layout: element.layout, order: index })
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDeleteAndSelect = (elementId: string) => {
|
const handleDeleteAndSelect = (elementId: string) => {
|
||||||
onElementDelete(elementId);
|
onElementDelete(elementId)
|
||||||
setEditingElement(null);
|
setEditingElement(null)
|
||||||
resetForm();
|
resetForm()
|
||||||
};
|
}
|
||||||
|
|
||||||
// const reordered = (reorderedElements: TemplateElement[]) => {
|
// const reordered = (reorderedElements: TemplateElement[]) => {
|
||||||
// onElementsReorder(reorderedElements);
|
// onElementsReorder(reorderedElements);
|
||||||
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
|
// onLayoutSettingsChange({ ...layoutSettings, showGrid: true });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full">
|
<div className="flex h-full w-full">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex flex-col bg-gray-50">
|
<div className="flex flex-1 flex-col bg-gray-50">
|
||||||
<div className="flex items-center justify-between p-2 border-b bg-white">
|
<div className="flex items-center justify-between border-b bg-white p-2">
|
||||||
<div className='px-2'>
|
<div className="px-2">
|
||||||
<h3 className="text-sm font-medium">Холст</h3>
|
<h3 className="text-sm font-medium">Холст</h3>
|
||||||
<p className="text-xs text-gray-500">Перетаскивайте элементы для настройки их расположения</p>
|
<p className="text-xs text-gray-500">
|
||||||
|
Перетаскивайте элементы для настройки их расположения
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Добавить элемент
|
Добавить элемент
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Добавить новый элемент</DialogTitle>
|
<DialogTitle>Добавить новый элемент</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -713,14 +816,22 @@ 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 onClick={() => setIsAddDialogOpen(false)} variant="outline">
|
<Button
|
||||||
|
onClick={() => setIsAddDialogOpen(false)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAddElement}
|
onClick={handleAddElement}
|
||||||
disabled={!formData.name || !formData.label || (formData.type !== 'standards' && !formData.targetCells?.length)}
|
disabled={
|
||||||
|
!formData.name ||
|
||||||
|
!formData.label ||
|
||||||
|
(formData.type !== 'standards' &&
|
||||||
|
!formData.targetCells?.length)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Добавить элемент
|
Добавить элемент
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -728,8 +839,11 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{editingElement && (
|
{editingElement && (
|
||||||
<Dialog open={!!editingElement} onOpenChange={() => handleCancelEdit()}>
|
<Dialog
|
||||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
open={!!editingElement}
|
||||||
|
onOpenChange={() => handleCancelEdit()}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-h-[80vh] max-w-4xl overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Редактировать элемент</DialogTitle>
|
<DialogTitle>Редактировать элемент</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -750,13 +864,13 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Button onClick={handleAutoArrange} variant="outline" size="sm">
|
<Button onClick={handleAutoArrange} variant="outline" size="sm">
|
||||||
<LayoutGrid className="h-4 w-4 mr-2" />
|
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||||
Расставить автоматически
|
Расставить автоматически
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
|
<Dialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm">
|
||||||
<Settings className="h-4 w-4 mr-2" />
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
Настройки холста
|
Настройки холста
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -767,7 +881,10 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
Настройте отображение сетки и другие параметры.
|
Настройте отображение сетки и другие параметры.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<CanvasSettings settings={layoutSettings} onChange={onLayoutSettingsChange} />
|
<CanvasSettings
|
||||||
|
settings={layoutSettings}
|
||||||
|
onChange={onLayoutSettingsChange}
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -784,5 +901,5 @@ export const ElementConstructor: React.FC<ElementConstructorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0 pointer-events-none"
|
className="absolute inset-0 pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(to right, #e5e7eb 1px, transparent 1px), linear-gradient(to bottom, #e5e7eb 1px, transparent 1px)`,
|
backgroundImage: `radial-gradient(circle, #e5e7eb 1px, transparent 1px)`,
|
||||||
backgroundSize: `${gridSize}px ${gridSize}px`,
|
backgroundSize: `${gridSize}px ${gridSize}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -301,6 +301,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
}, [onElementUpdate]);
|
}, [onElementUpdate]);
|
||||||
|
|
||||||
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
|
const handleCanvasClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
// Снимаем выделение при клике по пустому месту
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setSelectedElementId(null);
|
setSelectedElementId(null);
|
||||||
}
|
}
|
||||||
@@ -324,6 +325,7 @@ export const VisualLayoutEditor: React.FC<VisualLayoutEditorProps> = ({
|
|||||||
<div
|
<div
|
||||||
className="relative bg-white shadow-lg mx-auto my-8"
|
className="relative bg-white shadow-lg mx-auto my-8"
|
||||||
style={{ width: canvasSize.width, height: canvasSize.height }}
|
style={{ width: canvasSize.width, height: canvasSize.height }}
|
||||||
|
onClick={handleCanvasClick}
|
||||||
>
|
>
|
||||||
{GridPattern}
|
{GridPattern}
|
||||||
{elements.map((element) => (
|
{elements.map((element) => (
|
||||||
|
|||||||
@@ -1,116 +1,131 @@
|
|||||||
import { Plus, Wrench } from 'lucide-react';
|
import { Plus, Wrench } from 'lucide-react'
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor';
|
import { ElementConstructor } from '../../../../components/TemplateManager/ElementConstructor'
|
||||||
import { Button } from '../../../../components/ui/button';
|
import { Button } from '../../../../components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../../components/ui/card';
|
import {
|
||||||
import { useTemplateContext } from '../../../../contexts/TemplateContext';
|
Card,
|
||||||
import { FormLayoutSettings, Template, TemplateElement } from '../../../../types/template';
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '../../../../components/ui/card'
|
||||||
|
import { useTemplateContext } from '../../../../contexts/TemplateContext'
|
||||||
|
import {
|
||||||
|
FormLayoutSettings,
|
||||||
|
Template,
|
||||||
|
TemplateElement,
|
||||||
|
} from '../../../../types/template'
|
||||||
|
|
||||||
export const ElementsCreation: FC = () => {
|
export const ElementsCreation: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate()
|
||||||
const { templates, updateTemplate } = useTemplateContext();
|
const { templates, updateTemplate } = useTemplateContext()
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
|
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
const handleTemplateSelect = (template: Template) => {
|
const handleTemplateSelect = (template: Template) => {
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleElementAdd = (element: TemplateElement) => {
|
const handleElementAdd = (element: TemplateElement) => {
|
||||||
console.log('handleElementAdd called with element:', element);
|
console.log('handleElementAdd called with element:', element)
|
||||||
|
|
||||||
if (!selectedTemplate) {
|
if (!selectedTemplate) {
|
||||||
console.log('No selected template');
|
console.log('No selected template')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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('Updated template:', updatedTemplate)
|
||||||
console.log('Elements count:', updatedTemplate.elements.length);
|
console.log('Elements count:', updatedTemplate.elements.length)
|
||||||
|
|
||||||
updateTemplate(updatedTemplate);
|
updateTemplate(updatedTemplate)
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleElementUpdate = (elementId: string, updates: Partial<TemplateElement>) => {
|
const handleElementUpdate = (
|
||||||
if (!selectedTemplate) return;
|
elementId: string,
|
||||||
|
updates: Partial<TemplateElement>,
|
||||||
|
) => {
|
||||||
|
if (!selectedTemplate) return
|
||||||
|
|
||||||
const updatedTemplate = {
|
const updatedTemplate = {
|
||||||
...selectedTemplate,
|
...selectedTemplate,
|
||||||
elements: selectedTemplate.elements.map(el =>
|
elements: selectedTemplate.elements.map((el) =>
|
||||||
el.id === elementId ? { ...el, ...updates } : el
|
el.id === elementId ? { ...el, ...updates } : el,
|
||||||
),
|
),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
updateTemplate(updatedTemplate);
|
updateTemplate(updatedTemplate)
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleElementDelete = (elementId: string) => {
|
const handleElementDelete = (elementId: string) => {
|
||||||
if (!selectedTemplate) return;
|
if (!selectedTemplate) return
|
||||||
|
|
||||||
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(),
|
||||||
};
|
}
|
||||||
|
|
||||||
updateTemplate(updatedTemplate);
|
updateTemplate(updatedTemplate)
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleElementsReorder = (reorderedElements: TemplateElement[]) => {
|
const handleElementsReorder = (reorderedElements: TemplateElement[]) => {
|
||||||
if (!selectedTemplate) return;
|
if (!selectedTemplate) return
|
||||||
|
|
||||||
// Обновляем порядок элементов
|
// Обновляем порядок элементов
|
||||||
const elementsWithOrder = reorderedElements.map((element, index) => ({
|
const elementsWithOrder = reorderedElements.map((element, index) => ({
|
||||||
...element,
|
...element,
|
||||||
order: index,
|
order: index,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const updatedTemplate = {
|
const updatedTemplate = {
|
||||||
...selectedTemplate,
|
...selectedTemplate,
|
||||||
elements: elementsWithOrder,
|
elements: elementsWithOrder,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
updateTemplate(updatedTemplate);
|
updateTemplate(updatedTemplate)
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleLayoutSettingsChange = (settings: FormLayoutSettings) => {
|
const handleLayoutSettingsChange = (settings: FormLayoutSettings) => {
|
||||||
if (!selectedTemplate) return;
|
if (!selectedTemplate) return
|
||||||
|
|
||||||
const updatedTemplate = {
|
const updatedTemplate = {
|
||||||
...selectedTemplate,
|
...selectedTemplate,
|
||||||
layoutSettings: settings,
|
layoutSettings: settings,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
}
|
||||||
|
|
||||||
updateTemplate(updatedTemplate);
|
updateTemplate(updatedTemplate)
|
||||||
setSelectedTemplate(updatedTemplate);
|
setSelectedTemplate(updatedTemplate)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Настройки отображения по умолчанию
|
// Настройки отображения по умолчанию
|
||||||
const defaultLayoutSettings: FormLayoutSettings = {
|
const defaultLayoutSettings: FormLayoutSettings = {
|
||||||
gridSize: 20,
|
gridSize: 20,
|
||||||
showGrid: true,
|
showGrid: true,
|
||||||
};
|
}
|
||||||
|
|
||||||
if (selectedTemplate) {
|
if (selectedTemplate) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div className="bg-white border-b border-gray-200 p-4 sticky top-0 z-50">
|
<div className="sticky top-0 z-50 border-b border-gray-200 bg-white p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" onClick={handleBack}>
|
<Button variant="ghost" onClick={handleBack}>
|
||||||
@@ -120,26 +135,21 @@ export const ElementsCreation: FC = () => {
|
|||||||
<h1 className="text-xl font-semibold text-gray-800">
|
<h1 className="text-xl font-semibold text-gray-800">
|
||||||
Конструктор элементов: {selectedTemplate.name}
|
Конструктор элементов: {selectedTemplate.name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="mt-1 text-sm text-gray-600">
|
||||||
Создайте элементы формы и разместите их на холсте перетаскиванием
|
Создайте элементы формы и разместите их на холсте
|
||||||
|
перетаскиванием
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button onClick={() => navigate('/create-protocol')} variant="outline">
|
|
||||||
Предпросмотр формы
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => navigate('/templates')} variant="outline">
|
|
||||||
К шаблонам
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-[calc(100vh-104px)]">
|
<div className="h-[calc(100vh-104px)]">
|
||||||
<ElementConstructor
|
<ElementConstructor
|
||||||
elements={selectedTemplate.elements}
|
elements={selectedTemplate.elements}
|
||||||
layoutSettings={selectedTemplate.layoutSettings || defaultLayoutSettings}
|
layoutSettings={
|
||||||
|
selectedTemplate.layoutSettings || defaultLayoutSettings
|
||||||
|
}
|
||||||
onElementAdd={handleElementAdd}
|
onElementAdd={handleElementAdd}
|
||||||
onElementUpdate={handleElementUpdate}
|
onElementUpdate={handleElementUpdate}
|
||||||
onElementDelete={handleElementDelete}
|
onElementDelete={handleElementDelete}
|
||||||
@@ -148,15 +158,19 @@ export const ElementsCreation: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="mx-auto max-w-7xl p-6">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Создание элементов</h1>
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
<p className="text-gray-600 mt-2">Выберите шаблон для создания элементов интерфейса</p>
|
Создание элементов
|
||||||
|
</h1>
|
||||||
|
<p className="mt-2 text-gray-600">
|
||||||
|
Выберите шаблон для создания элементов интерфейса
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -170,19 +184,26 @@ export const ElementsCreation: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="py-12 text-center">
|
||||||
<Wrench className="h-16 w-16 text-gray-400 mx-auto mb-4" />
|
<Wrench className="mx-auto mb-4 h-16 w-16 text-gray-400" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Нет доступных шаблонов</h3>
|
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||||||
<p className="text-gray-600 mb-6">Создайте шаблон протокола для начала работы с элементами</p>
|
Нет доступных шаблонов
|
||||||
|
</h3>
|
||||||
|
<p className="mb-6 text-gray-600">
|
||||||
|
Создайте шаблон протокола для начала работы с элементами
|
||||||
|
</p>
|
||||||
<Button onClick={() => navigate('/templates')}>
|
<Button onClick={() => navigate('/templates')}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Создать шаблон
|
Создать шаблон
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<Card key={template.id} className="hover:shadow-lg transition-shadow cursor-pointer">
|
<Card
|
||||||
|
key={template.id}
|
||||||
|
className="cursor-pointer transition-shadow hover:shadow-lg"
|
||||||
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">{template.name}</CardTitle>
|
<CardTitle className="text-lg">{template.name}</CardTitle>
|
||||||
{template.description && (
|
{template.description && (
|
||||||
@@ -197,7 +218,7 @@ export const ElementsCreation: FC = () => {
|
|||||||
<Wrench className="h-4 w-4" />
|
<Wrench className="h-4 w-4" />
|
||||||
<span>{template.elements.length} элементов создано</span>
|
<span>{template.elements.length} элементов создано</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
|
Обновлен: {template.updatedAt.toLocaleDateString('ru-RU')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,5 +236,5 @@ export const ElementsCreation: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ const ProtocolForm: FC<ProtocolFormProps> = ({ template, onSave, onBack }) => {
|
|||||||
{/* Полноэкранная форма */}
|
{/* Полноэкранная форма */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<div
|
<div
|
||||||
className="relative bg-white shadow-lg mx-auto my-8"
|
className="relative mx-auto my-8"
|
||||||
style={{ width: canvasSize.width, height: canvasSize.height }}
|
style={{ width: canvasSize.width, height: canvasSize.height }}
|
||||||
>
|
>
|
||||||
{template.elements.map((element) => {
|
{template.elements.map((element) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user