From 3fc352f8e9fcc40af477b103a45f6bb9af092e37 Mon Sep 17 00:00:00 2001 From: tlartem Date: Tue, 22 Jul 2025 19:44:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=95=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D1=8D=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/app/App.tsx | 43 +- src/app/index.tsx | 4 +- .../CalibrationConditionsElement.tsx | 5 +- .../BasicElements/CheckboxElement.tsx | 6 +- src/component/BasicElements/DateElement.tsx | 4 +- src/component/BasicElements/NumberElement.tsx | 4 +- src/component/BasicElements/RadioElement.tsx | 4 +- src/component/BasicElements/SelectElement.tsx | 4 +- src/component/BasicElements/TextElement.tsx | 45 -- .../BasicElements/TextareaElement.tsx | 4 +- src/component/BasicElements/index.ts | 4 +- .../DualSpreadsheet/components/FormulaBar.tsx | 21 +- .../components/Spreadsheet.tsx | 15 +- src/component/StandardsElement/definition.tsx | 3 +- .../TemplateManager/AutocompleteInput.tsx | 177 ++++++++ .../CategoryFieldsSelector.tsx | 134 ++++++ .../TemplateManager/ElementConstructor.tsx | 153 ++----- .../TemplateManager/TemplateFilterSidebar.tsx | 225 ++++++++++ .../TemplateManager/VisualLayoutEditor.tsx | 276 +++--------- src/component/ui/sidebar.tsx | 416 ++++++++++++++++++ .../model/implementations/ButtonGroup.tsx} | 83 ++-- .../model/implementations/TextElement.tsx | 53 +++ src/entitiy/element/model/interface.ts | 91 ++++ src/entitiy/element/ui/extraSettingsBadge.tsx | 7 + .../template/api/templateApiService.ts | 72 ++- .../template/model/CategoryContext.tsx | 289 ++++++++++++ .../template/model/TemplateContext.tsx | 5 + src/lib/element-registry-init.ts | 30 -- src/lib/element-registry.ts | 48 -- src/page/ProtocolCreation/Page.tsx | 166 +++++-- src/page/TemplatesOverviewPage.tsx | 320 ++++++++++---- src/type/template.ts | 50 ++- src/widget/template/ui/TemplateCard.tsx | 17 +- vite.config.ts | 2 +- yarn.lock | 2 +- 36 files changed, 2108 insertions(+), 675 deletions(-) delete mode 100644 src/component/BasicElements/TextElement.tsx create mode 100644 src/component/TemplateManager/AutocompleteInput.tsx create mode 100644 src/component/TemplateManager/CategoryFieldsSelector.tsx create mode 100644 src/component/TemplateManager/TemplateFilterSidebar.tsx create mode 100644 src/component/ui/sidebar.tsx rename src/{component/BasicElements/ButtonGroupElement.tsx => entitiy/element/model/implementations/ButtonGroup.tsx} (75%) create mode 100644 src/entitiy/element/model/implementations/TextElement.tsx create mode 100644 src/entitiy/element/model/interface.ts create mode 100644 src/entitiy/element/ui/extraSettingsBadge.tsx create mode 100644 src/entitiy/template/model/CategoryContext.tsx delete mode 100644 src/lib/element-registry-init.ts delete mode 100644 src/lib/element-registry.ts diff --git a/package.json b/package.json index 9123398..95a6777 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.14", diff --git a/src/app/App.tsx b/src/app/App.tsx index a2c15cf..0d31aa0 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,6 +2,7 @@ import { FC, useEffect } from 'react' import { Navigate, Route, Routes } from 'react-router-dom' +import { CategoryProvider } from '@/entitiy/template/model/CategoryContext' import { TemplateProvider } from '@/entitiy/template/model/TemplateContext' import { setGlobalToastContext, @@ -33,26 +34,28 @@ const App: FC = () => { - - - }> - } /> - } /> - } - /> - } - /> - } - /> - - - + + + + }> + } /> + } /> + } + /> + } + /> + } + /> + + + + ) diff --git a/src/app/index.tsx b/src/app/index.tsx index efc9e42..84f5e47 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -4,7 +4,7 @@ import { BrowserRouter } from 'react-router-dom' import { store } from './store' -import { initializeElementRegistry } from '../lib/element-registry-init' +import { initializeElementRegistry } from '@/entitiy/element/model/interface' import App from './App' import './index.css' @@ -17,5 +17,5 @@ createRoot(document.getElementById('root') as HTMLElement).render( - , + ) diff --git a/src/component/BasicElements/CalibrationConditionsElement.tsx b/src/component/BasicElements/CalibrationConditionsElement.tsx index 7d7cbc1..8a726a6 100644 --- a/src/component/BasicElements/CalibrationConditionsElement.tsx +++ b/src/component/BasicElements/CalibrationConditionsElement.tsx @@ -3,7 +3,7 @@ import { Button } from '@/component/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/component/ui/card' import { Input } from '@/component/ui/input' import { Label } from '@/component/ui/label' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { AlertCircle, Check, @@ -418,9 +418,10 @@ export const calibrationConditionsDefinition: ElementDefinition< CalibrationConditionsConfig, CalibrationConditions > = { - type: 'calibration-conditions', + type: 'calibration_conditions', label: 'Условия калибровки', icon: , + version: 1, defaultConfig: { targetCells: [], defaultConditions: { diff --git a/src/component/BasicElements/CheckboxElement.tsx b/src/component/BasicElements/CheckboxElement.tsx index 430732e..089bc0f 100644 --- a/src/component/BasicElements/CheckboxElement.tsx +++ b/src/component/BasicElements/CheckboxElement.tsx @@ -1,4 +1,4 @@ -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { CheckSquare } from 'lucide-react' interface CheckboxConfig { @@ -11,11 +11,13 @@ export const checkboxDefinition: ElementDefinition = { type: 'checkbox', label: 'Чекбокс', icon: , + version: 1, defaultConfig: { placeholder: '', required: false, targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: () => (
Дополнительные настройки отсутствуют @@ -37,7 +39,7 @@ export const checkboxDefinition: ElementDefinition = { onChange?.(e.target.checked)} + onChange={e => onChange?.(e.target.checked)} required={config.required} className="h-4 w-4" /> diff --git a/src/component/BasicElements/DateElement.tsx b/src/component/BasicElements/DateElement.tsx index b0bab50..e01c45a 100644 --- a/src/component/BasicElements/DateElement.tsx +++ b/src/component/BasicElements/DateElement.tsx @@ -1,5 +1,5 @@ import { Input } from '@/component/ui/input' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { Calendar } from 'lucide-react' interface DateConfig { @@ -11,10 +11,12 @@ export const dateDefinition: ElementDefinition = { type: 'date', label: 'Дата', icon: , + version: 1, defaultConfig: { required: false, targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: ({ config, onChange }) => (
diff --git a/src/component/BasicElements/NumberElement.tsx b/src/component/BasicElements/NumberElement.tsx index b23b1cb..14b2e57 100644 --- a/src/component/BasicElements/NumberElement.tsx +++ b/src/component/BasicElements/NumberElement.tsx @@ -1,5 +1,5 @@ import { Input } from '@/component/ui/input' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { Hash } from 'lucide-react' interface NumberConfig { @@ -12,11 +12,13 @@ export const numberDefinition: ElementDefinition = { type: 'number', label: 'Число', icon: , + version: 1, defaultConfig: { placeholder: '', required: false, targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: () => (
Дополнительные настройки отсутствуют diff --git a/src/component/BasicElements/RadioElement.tsx b/src/component/BasicElements/RadioElement.tsx index 15d2598..eea572e 100644 --- a/src/component/BasicElements/RadioElement.tsx +++ b/src/component/BasicElements/RadioElement.tsx @@ -1,6 +1,6 @@ import { Button } from '@/component/ui/button' import { Input } from '@/component/ui/input' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { ElementOption } from '@/type/template' import { CheckSquare, Plus, Trash2 } from 'lucide-react' @@ -15,12 +15,14 @@ export const radioDefinition: ElementDefinition = { type: 'radio', label: 'Радиокнопки', icon: , + version: 1, defaultConfig: { placeholder: '', required: false, options: [], targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: ({ config, onChange }) => { const handleAddOption = () => { onChange({ diff --git a/src/component/BasicElements/SelectElement.tsx b/src/component/BasicElements/SelectElement.tsx index c2c76ad..dcb94c4 100644 --- a/src/component/BasicElements/SelectElement.tsx +++ b/src/component/BasicElements/SelectElement.tsx @@ -7,7 +7,7 @@ import { SelectTrigger, SelectValue, } from '@/component/ui/select' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { ElementOption } from '@/type/template' import { ChevronDown, Plus, Trash2 } from 'lucide-react' @@ -22,12 +22,14 @@ export const selectDefinition: ElementDefinition = { type: 'select', label: 'Выпадающий список', icon: , + version: 1, defaultConfig: { placeholder: '', required: false, options: [], targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: ({ config, onChange }) => { const handleAddOption = () => { onChange({ diff --git a/src/component/BasicElements/TextElement.tsx b/src/component/BasicElements/TextElement.tsx deleted file mode 100644 index e8b8364..0000000 --- a/src/component/BasicElements/TextElement.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Input } from '@/component/ui/input' -import { ElementDefinition } from '@/lib/element-registry' -import { Type } from 'lucide-react' - -interface TextConfig { - placeholder?: string - required?: boolean - targetCells: any[] -} - -export const textDefinition: ElementDefinition = { - type: 'text', - label: 'Текстовое поле', - icon: , - defaultConfig: { - placeholder: '', - required: false, - targetCells: [], - }, - Editor: () => ( -
- Дополнительные настройки отсутствуют -
- ), - Preview: ({ config }) => ( -
-
Предпросмотр
-
- -
-
- ), - Render: ({ config, value, onChange }) => ( - onChange?.(e.target.value)} - required={config.required} - /> - ), -} diff --git a/src/component/BasicElements/TextareaElement.tsx b/src/component/BasicElements/TextareaElement.tsx index 3f8575d..f1da0b0 100644 --- a/src/component/BasicElements/TextareaElement.tsx +++ b/src/component/BasicElements/TextareaElement.tsx @@ -1,5 +1,5 @@ import { Textarea } from '@/component/ui/textarea' -import { ElementDefinition } from '@/lib/element-registry' +import { ElementDefinition } from '@/entitiy/element/model/interface' import { FileText } from 'lucide-react' interface TextareaConfig { @@ -12,11 +12,13 @@ export const textareaDefinition: ElementDefinition = { type: 'textarea', label: 'Многострочный текст', icon: , + version: 1, defaultConfig: { placeholder: '', required: false, targetCells: [], }, + mapToCells: config => config.targetCells || [], Editor: () => (
Дополнительные настройки отсутствуют diff --git a/src/component/BasicElements/index.ts b/src/component/BasicElements/index.ts index a0d54fe..2b7dfc2 100644 --- a/src/component/BasicElements/index.ts +++ b/src/component/BasicElements/index.ts @@ -1,4 +1,5 @@ -export { buttonGroupDefinition } from './ButtonGroupElement' +export { textDefinition } from '@/entitiy/element/model/implementations/TextElement' +export { buttonGroupDefinition } from '../../entitiy/element/model/implementations/ButtonGroup' export { calibrationConditionsDefinition } from './CalibrationConditionsElement' export { checkboxDefinition } from './CheckboxElement' export { dateDefinition } from './DateElement' @@ -6,4 +7,3 @@ export { numberDefinition } from './NumberElement' export { radioDefinition } from './RadioElement' export { selectDefinition } from './SelectElement' export { textareaDefinition } from './TextareaElement' -export { textDefinition } from './TextElement' diff --git a/src/component/DualSpreadsheet/components/FormulaBar.tsx b/src/component/DualSpreadsheet/components/FormulaBar.tsx index 4118977..6bfeef5 100644 --- a/src/component/DualSpreadsheet/components/FormulaBar.tsx +++ b/src/component/DualSpreadsheet/components/FormulaBar.tsx @@ -1,4 +1,4 @@ -import { Save } from 'lucide-react' +import { Redo2, Save, Undo2 } from 'lucide-react' import { memo } from 'react' import { FormulaBarProps } from '../types' import { getColumnLabel } from '../utils' @@ -144,6 +144,25 @@ export const FormulaBar = memo( )}
)} + {/* Кнопки отмены и повтора */} + { +
+ + +
+ } {/* Кнопка ручного сохранения */} {onManualSave && (
diff --git a/src/component/StandardsElement/definition.tsx b/src/component/StandardsElement/definition.tsx index 8501a8b..b0de741 100644 --- a/src/component/StandardsElement/definition.tsx +++ b/src/component/StandardsElement/definition.tsx @@ -1,5 +1,5 @@ +import { ElementDefinition } from '@/entitiy/element/model/interface' import { parseCellAddress } from '@/lib/cell-utils' -import { ElementDefinition } from '@/lib/element-registry' import { CellTarget } from '@/type/template' import { Weight } from 'lucide-react' import { StandardsEditor } from './StandardsEditor' @@ -18,6 +18,7 @@ export const standardsDefinition: ElementDefinition = type: 'standards', label: 'Измерительные эталоны', icon: , + version: 1, defaultConfig: { registryNumber: '', sheet: 'Report', diff --git a/src/component/TemplateManager/AutocompleteInput.tsx b/src/component/TemplateManager/AutocompleteInput.tsx new file mode 100644 index 0000000..6d4722c --- /dev/null +++ b/src/component/TemplateManager/AutocompleteInput.tsx @@ -0,0 +1,177 @@ +import { Button } from '@/component/ui/button' +import { Input } from '@/component/ui/input' +import { Label } from '@/component/ui/label' +import { ChevronDown, X } from 'lucide-react' +import React, { useEffect, useRef, useState } from 'react' + +interface AutocompleteInputProps { + label: string + value: string + onValueChange: (value: string) => void + suggestions: string[] + placeholder?: string + disabled?: boolean +} + +export const AutocompleteInput: React.FC = ({ + label, + value, + onValueChange, + suggestions, + placeholder = '', + disabled = false, +}) => { + const [isOpen, setIsOpen] = useState(false) + const [filteredSuggestions, setFilteredSuggestions] = useState([]) + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const inputRef = useRef(null) + const dropdownRef = useRef(null) + + // Фильтруем предложения на основе введенного текста + useEffect(() => { + if (value.trim() === '') { + setFilteredSuggestions(suggestions) + } else { + const filtered = suggestions.filter(suggestion => + suggestion.toLowerCase().includes(value.toLowerCase()) + ) + setFilteredSuggestions(filtered) + } + setHighlightedIndex(-1) + }, [value, suggestions]) + + // Закрытие выпадающего списка при клике вне компонента + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + inputRef.current && + !inputRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const handleInputChange = (e: React.ChangeEvent) => { + onValueChange(e.target.value) + setIsOpen(true) + } + + const handleSuggestionClick = (suggestion: string) => { + onValueChange(suggestion) + setIsOpen(false) + inputRef.current?.blur() + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!isOpen || filteredSuggestions.length === 0) return + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setHighlightedIndex(prev => + prev < filteredSuggestions.length - 1 ? prev + 1 : 0 + ) + break + case 'ArrowUp': + e.preventDefault() + setHighlightedIndex(prev => + prev > 0 ? prev - 1 : filteredSuggestions.length - 1 + ) + break + case 'Enter': + e.preventDefault() + if (highlightedIndex >= 0) { + handleSuggestionClick(filteredSuggestions[highlightedIndex]) + } + break + case 'Escape': + setIsOpen(false) + inputRef.current?.blur() + break + } + } + + const clearValue = () => { + onValueChange('') + inputRef.current?.focus() + } + + return ( +
+ +
+
+ setIsOpen(true)} + placeholder={placeholder} + disabled={disabled} + className="pr-16" + /> +
+ {value && ( + + )} + +
+
+ + {/* Выпадающий список предложений */} + {isOpen && filteredSuggestions.length > 0 && !disabled && ( +
+ {filteredSuggestions.map((suggestion, index) => ( +
handleSuggestionClick(suggestion)} + onMouseEnter={() => setHighlightedIndex(index)} + > + {suggestion} +
+ ))} +
+ )} +
+
+ ) +} + +export default AutocompleteInput diff --git a/src/component/TemplateManager/CategoryFieldsSelector.tsx b/src/component/TemplateManager/CategoryFieldsSelector.tsx new file mode 100644 index 0000000..ca9c2f2 --- /dev/null +++ b/src/component/TemplateManager/CategoryFieldsSelector.tsx @@ -0,0 +1,134 @@ +import { Input } from '@/component/ui/input' +import { Label } from '@/component/ui/label' +import { useCategoryContext } from '@/entitiy/template/model/CategoryContext' +import { Attribute } from '@/type/template' +import { AlertCircle } from 'lucide-react' +import React from 'react' + +interface AttributeValue { + attributeId: string + value: string | number +} + +interface CategoryFieldsSelectorProps { + selectedValues: AttributeValue[] + onValuesChange: (values: AttributeValue[]) => void + disabled?: boolean +} + +export const CategoryFieldsSelector: React.FC = ({ + selectedValues, + onValuesChange, + disabled = false, +}) => { + const { + attributes, + getRequiredAttributes, + getOptionalAttributes, + isLoading, + } = useCategoryContext() + + const requiredAttributes = getRequiredAttributes() + const optionalAttributes = getOptionalAttributes() + + const getAttributeValue = (attributeId: string): string => { + const value = selectedValues.find(v => v.attributeId === attributeId)?.value + return value?.toString() || '' + } + + const updateAttributeValue = ( + attributeId: string, + value: string | number + ) => { + const newValues = selectedValues.filter(v => v.attributeId !== attributeId) + if (value !== '' && value !== null && value !== undefined) { + newValues.push({ attributeId, value }) + } + onValuesChange(newValues) + } + + const renderAttribute = (attribute: Attribute) => { + const value = getAttributeValue(attribute.id) + + const fieldProps = { + disabled, + } + + return ( +
+ + + {/* Простое текстовое поле для всех атрибутов */} + updateAttributeValue(attribute.id, e.target.value)} + placeholder={`Введите ${attribute.name.toLowerCase()}`} + {...fieldProps} + /> + + {attribute.is_required && !value && ( +

+ + Это поле обязательно для заполнения +

+ )} +
+ ) + } + + if (isLoading) { + return ( +
+
+ Загрузка атрибутов... +
+
+ ) + } + + return ( +
+ {/* Обязательные атрибуты */} + {requiredAttributes.length > 0 && ( +
+
+ +
+
+ {requiredAttributes.map(renderAttribute)} +
+
+ )} + + {/* Необязательные атрибуты */} + {optionalAttributes.length > 0 && ( +
+
+ +
+
+ {optionalAttributes.map(renderAttribute)} +
+
+ )} + + {attributes.length === 0 && ( +
+

Атрибуты не настроены

+
+ )} +
+ ) +} + +export default CategoryFieldsSelector diff --git a/src/component/TemplateManager/ElementConstructor.tsx b/src/component/TemplateManager/ElementConstructor.tsx index c715198..f24cef8 100644 --- a/src/component/TemplateManager/ElementConstructor.tsx +++ b/src/component/TemplateManager/ElementConstructor.tsx @@ -1,7 +1,7 @@ import { getAvailableElementTypes, getElementDefinition, -} from '@/lib/element-registry' +} from '@/entitiy/element/model/interface' import { autoArrangeElements, findFreePosition, @@ -109,8 +109,6 @@ const CellTargetEditor: React.FC<{ L (Левый) R (Правый) - Report - Calculations
@@ -254,7 +252,7 @@ const ElementPreview: React.FC<{ element: Partial }> = ({ ) - case 'calibration-conditions': + case 'calibration_conditions': return (
@@ -287,7 +285,7 @@ const ElementPreview: React.FC<{ element: Partial }> = ({
) - case 'button-group': + case 'button_group': return (
@@ -381,7 +379,7 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { const needsOptions = formData.type === 'select' || formData.type === 'radio' || - formData.type === 'button-group' + formData.type === 'button_group' // Если есть определение элемента в реестре, используем его редактор if (elementDefinition && elementDefinition.Editor) { @@ -395,35 +393,21 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { value={formData.type} onValueChange={value => { const newType = value as ElementType - // console.log('Element type changed to:', newType) + const elementDefinition = getElementDefinition(newType) setFormData(prev => { - const updates: Partial = { type: newType } - - // Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию - if ( - newType === 'standards' || - newType === 'calibration-conditions' - ) { - // console.log( - // `${newType} type selected, getting element definition` - // ) - const elementDefinition = getElementDefinition(newType) - // console.log('Element definition:', elementDefinition) - - if (elementDefinition && elementDefinition.defaultConfig) { - const defaultConfig = - elementDefinition.defaultConfig as any - // console.log('Default config:', defaultConfig) - updates.targetCells = - elementDefinition.mapToCells?.(defaultConfig) || [] - // console.log('Generated targetCells:', updates.targetCells) - } + const updates: Partial = { + type: newType, + // Сбрасываем targetCells при смене типа + targetCells: [], } - const newFormData = { ...prev, ...updates } - // console.log('New form data:', newFormData) - return newFormData + // Применяем конфиг по умолчанию из определения элемента + if (elementDefinition && elementDefinition.defaultConfig) { + Object.assign(updates, elementDefinition.defaultConfig) + } + + return { ...prev, ...updates } }) }} > @@ -520,40 +504,21 @@ const ElementForm: React.FC = ({ formData, setFormData }) => { value={formData.type} onValueChange={value => { const newType = value as ElementType - // console.log('Element type changed to (fallback):', newType) + const elementDefinition = getElementDefinition(newType) setFormData(prev => { - const updates: Partial = { type: newType } - - // Если выбран тип "standards" или "calibration-conditions", устанавливаем значения по умолчанию - if ( - newType === 'standards' || - newType === 'calibration-conditions' - ) { - // console.log( - // `${newType} type selected (fallback), getting element definition` - // ) - const elementDefinition = getElementDefinition(newType) - // console.log( - // 'Element definition (fallback):', - // elementDefinition - // ) - - if (elementDefinition && elementDefinition.defaultConfig) { - const defaultConfig = elementDefinition.defaultConfig as any - // console.log('Default config (fallback):', defaultConfig) - updates.targetCells = - elementDefinition.mapToCells?.(defaultConfig) || [] - // console.log( - // 'Generated targetCells (fallback):', - // updates.targetCells - // ) - } + const updates: Partial = { + type: newType, + // Сбрасываем targetCells при смене типа + targetCells: [], } - const newFormData = { ...prev, ...updates } - // console.log('New form data (fallback):', newFormData) - return newFormData + // Применяем конфиг по умолчанию из определения элемента + if (elementDefinition && elementDefinition.defaultConfig) { + Object.assign(updates, elementDefinition.defaultConfig) + } + + return { ...prev, ...updates } }) }} > @@ -760,23 +725,14 @@ export const ElementConstructor: React.FC = ({ } const handleAddElement = () => { - // console.log('handleAddElement called with formData:', formData) - if (!formData.label) { - // console.log('Validation failed: missing label') return } - // Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны - if ( - formData.type !== 'standards' && - formData.type !== 'calibration-conditions' && - formData.type !== 'button-group' && - !formData.targetCells?.length - ) { - // console.log( - // 'Validation failed: missing targetCells for element that requires manual targetCells' - // ) + // Получаем определение элемента + const elementDefinition = getElementDefinition(formData.type as ElementType) + if (!elementDefinition) { + console.error(`Element definition for ${formData.type} not found`) return } @@ -790,25 +746,10 @@ export const ElementConstructor: React.FC = ({ layoutSettings.gridSize ) - // Специальная обработка для элементов с автоматической генерацией targetCells + // Используем targetCells из формы или генерируем через mapToCells let targetCells = formData.targetCells || [] - if ( - formData.type === 'standards' || - formData.type === 'calibration-conditions' - ) { - // Генерируем targetCells автоматически, если их нет - if (!targetCells.length) { - const elementDefinition = getElementDefinition(formData.type) - if (elementDefinition && elementDefinition.defaultConfig) { - const defaultConfig = elementDefinition.defaultConfig as any - targetCells = elementDefinition.mapToCells?.(defaultConfig) || [] - } - } - } - - // Для button-group не нужны targetCells, так как это UI элемент - if (formData.type === 'button-group') { - targetCells = [] + if (!targetCells.length) { + targetCells = elementDefinition.mapToCells(formData as any) || [] } const newElement: TemplateElement = { @@ -837,14 +778,20 @@ export const ElementConstructor: React.FC = ({ const handleUpdateElement = () => { if (!editingElement || !formData.label) return - // Для элементов типа "standards", "calibration-conditions" и "button-group" не требуем targetCells, так как они генерируются автоматически или не нужны - if ( - formData.type !== 'standards' && - formData.type !== 'calibration-conditions' && - formData.type !== 'button-group' && - !formData.targetCells?.length + // Получаем определение элемента + 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, @@ -852,6 +799,7 @@ export const ElementConstructor: React.FC = ({ formData.label?.toLowerCase().replace(/\s+/g, '_') || formData.name || '', + targetCells, } onElementUpdate(editingElement.id, updatedData) @@ -943,14 +891,7 @@ export const ElementConstructor: React.FC = ({ + )} +
+ + + + {/* Фильтр по названию */} + + + + updateFilters({ name: value })} + suggestions={nameSuggestions} + placeholder="Введите название" + /> + + + + + + {/* Фильтр по описанию */} + + + + updateFilters({ description: value })} + suggestions={descriptionSuggestions} + placeholder="Введите описание" + /> + + + + + + {/* Активные фильтры по атрибутам */} + {Object.keys(filters.attributes).length > 0 && ( + <> + + Активные фильтры + +
+ {Object.entries(filters.attributes).map( + ([attributeId, value]) => ( +
+ + {getAttributeName(attributeId)}: + +
+ {value} + +
+
+ ) + )} +
+
+
+ + + )} + + {/* Фильтры по атрибутам */} + {!attributesLoading && attributes.length > 0 && ( + + {/* Фильтры по атрибутам */} + +
+ {attributes.map(attribute => ( +
+ + updateAttributeFilter(attribute.id, value) + } + suggestions={[]} // Пока без автодополнения + placeholder={`Фильтр по ${attribute.name.toLowerCase()}`} + /> +
+ ))} +
+
+
+ )} + + {attributesLoading && ( + + Атрибуты + +
+ Загрузка атрибутов... +
+
+
+ )} + + {!attributesLoading && attributes.length === 0 && ( + + Атрибуты + +
+ Атрибуты не настроены +
+
+
+ )} +
+ + ) +} + +export default TemplateFilterSidebar diff --git a/src/component/TemplateManager/VisualLayoutEditor.tsx b/src/component/TemplateManager/VisualLayoutEditor.tsx index 34c1719..174b75c 100644 --- a/src/component/TemplateManager/VisualLayoutEditor.tsx +++ b/src/component/TemplateManager/VisualLayoutEditor.tsx @@ -1,33 +1,19 @@ -import { - Calendar, - ChevronDown, - Droplets, - Edit, - Edit3, - Gauge, - Grid, - Move, - Radio, - Thermometer, - Trash2, - Zap, -} from 'lucide-react' +import { Edit, Grid, Move, Trash2 } from 'lucide-react' import React, { useCallback, useMemo, useState } from 'react' import { Rnd } from 'react-rnd' +import { getElementDefinition } from '../../entitiy/element/model/interface' import { ElementLayout, FormLayoutSettings, TemplateElement, } from '../../type/template' import { Button } from '../ui/button' -import { Input } from '../ui/input' -import { Textarea } from '../ui/textarea' // Минимальные размеры для разных типов элементов. // Если понадобится скорректировать размеры для конкретного элемента, // достаточно изменить эту таблицу. const ELEMENT_MIN_SIZE: Record = { - text: { width: 120, height: 40 }, + text: { width: 120, height: 80 }, textarea: { width: 160, height: 80 }, number: { width: 120, height: 40 }, date: { width: 140, height: 40 }, @@ -35,8 +21,8 @@ const ELEMENT_MIN_SIZE: Record = { radio: { width: 160, height: 80 }, checkbox: { width: 140, height: 40 }, standards: { width: 200, height: 50 }, - 'calibration-conditions': { width: 250, height: 60 }, - 'button-group': { width: 180, height: 50 }, + calibration_conditions: { width: 250, height: 60 }, + button_group: { width: 180, height: 80 }, } interface VisualLayoutEditorProps { @@ -61,26 +47,6 @@ interface DraggableElementProps { gridSize: number } -// const ELEMENT_ICONS: Record = { -// text: , -// textarea: , -// number: , -// date: , -// select: , -// radio: , -// checkbox: , -// }; - -// const ELEMENT_COLORS: Record = { -// text: 'bg-blue-100 border-blue-300 text-blue-800', -// textarea: 'bg-green-100 border-green-300 text-green-800', -// number: 'bg-purple-100 border-purple-300 text-purple-800', -// date: 'bg-orange-100 border-orange-300 text-orange-800', -// select: 'bg-indigo-100 border-indigo-300 text-indigo-800', -// radio: 'bg-pink-100 border-pink-300 text-pink-800', -// checkbox: 'bg-teal-100 border-teal-300 text-teal-800', -// }; - const DraggableElement: React.FC = React.memo( ({ element, isSelected, onSelect, onUpdate, onDelete, onEdit, gridSize }) => { const layout = element.layout || { @@ -145,141 +111,16 @@ const DraggableElement: React.FC = React.memo( * behaviour of Rnd and keeps rendering costs minimal. */ const previewControl = useMemo(() => { - switch (element.type) { - case 'text': - return ( - - ) - case 'textarea': - return ( -