дизайн групп

This commit is contained in:
2025-08-04 20:43:43 +03:00
parent 4d69217967
commit f66499467c
2 changed files with 426 additions and 287 deletions

View File

@@ -36,7 +36,12 @@ interface DropZoneProps {
isDragActive?: boolean
}
const DropZone: React.FC<DropZoneProps> = ({ id, position, isVisible = true, isDragActive = false }) => {
const DropZone: React.FC<DropZoneProps> = ({
id,
position,
isVisible = true,
isDragActive = false,
}) => {
const { setNodeRef, isOver } = useDroppable({
id,
})
@@ -47,7 +52,7 @@ const DropZone: React.FC<DropZoneProps> = ({ id, position, isVisible = true, isD
<div
ref={setNodeRef}
className={clsx(
'absolute top-0 bottom-0 w-3 z-10',
'absolute bottom-0 top-0 z-10 w-3',
position === 'left' ? '-left-3' : '-right-3',
'transition-all duration-200 ease-in-out',
// Базовое состояние - полностью прозрачная зона
@@ -59,19 +64,23 @@ const DropZone: React.FC<DropZoneProps> = ({ id, position, isVisible = true, isD
{/* Активный индикатор только при drag over */}
{isOver && (
<div className="absolute inset-0 flex items-center justify-center">
<div className={clsx(
'h-full w-1 bg-primary rounded-full shadow-sm',
'animate-in fade-in duration-200',
// Добавляем тонкий border для лучшей видимости
'ring-1 ring-primary/20'
)}>
<div
className={clsx(
'h-full w-1 rounded-full bg-primary shadow-sm',
'duration-200 animate-in fade-in',
// Добавляем тонкий border для лучшей видимости
'ring-1 ring-primary/20'
)}
>
{/* Индикатор направления вставки */}
<div className="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 left-1/2">
<div className={clsx(
'w-2 h-2 bg-primary rounded-full',
'border border-background shadow-sm',
'animate-pulse'
)} />
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<div
className={clsx(
'h-2 w-2 rounded-full bg-primary',
'border border-background shadow-sm',
'animate-pulse'
)}
/>
</div>
</div>
</div>
@@ -105,7 +114,9 @@ const DraggableDroppableTemplateCard: React.FC<DraggableTemplateCardProps> = ({
})
const style = {
transform: dragTransform ? `translate3d(${dragTransform.x}px, ${dragTransform.y}px, 0)` : undefined,
transform: dragTransform
? `translate3d(${dragTransform.x}px, ${dragTransform.y}px, 0)`
: undefined,
zIndex: isDragging ? 1000 : 'auto',
opacity: isDragging ? 0.5 : 1,
}
@@ -114,7 +125,7 @@ const DraggableDroppableTemplateCard: React.FC<DraggableTemplateCardProps> = ({
<div className="relative">
{/* Drop зона слева */}
<DropZone id={`drop-left-${template.id}`} position="left" />
{/* Основная карточка */}
<div
ref={setDragRef}
@@ -122,7 +133,7 @@ const DraggableDroppableTemplateCard: React.FC<DraggableTemplateCardProps> = ({
{...dragAttributes}
{...dragListeners}
className={clsx(
'transition-shadow duration-200 cursor-grab active:cursor-grabbing relative',
'relative cursor-grab transition-shadow duration-200 active:cursor-grabbing',
isDragging && 'shadow-xl'
)}
>
@@ -137,7 +148,7 @@ const DraggableDroppableTemplateCard: React.FC<DraggableTemplateCardProps> = ({
activeFilters={activeFilters}
/>
</div>
{/* Drop зона справа */}
<DropZone id={`drop-right-${template.id}`} position="right" />
</div>
@@ -179,11 +190,11 @@ const DroppableGroup: React.FC<DroppableGroupProps> = ({
<div
ref={setNodeRef}
className={clsx(
'transition-colors duration-200 relative',
templates.length === 0
? 'min-h-[120px] flex items-center justify-center'
: 'grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6',
isOver && templates.length === 0 && 'bg-primary/5 rounded-lg'
'relative transition-colors duration-200',
templates.length === 0
? 'flex min-h-[120px] items-center justify-center'
: 'grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6',
isOver && templates.length === 0 && 'rounded-lg bg-primary/5'
)}
>
{templates.length > 0 ? (
@@ -203,21 +214,25 @@ const DroppableGroup: React.FC<DroppableGroupProps> = ({
))
) : (
/* Минималистичный placeholder для пустой группы */
<div className={clsx(
'flex items-center justify-center py-12 text-center rounded-lg transition-all duration-300',
'border border-dashed border-muted-foreground/15',
isOver
? 'border-primary/50 bg-primary/5 scale-[1.02]'
: 'hover:border-muted-foreground/25 hover:bg-muted/20'
)}>
<div
className={clsx(
'flex items-center justify-center rounded-lg py-12 text-center transition-all duration-300',
'border border-dashed border-muted-foreground/15',
isOver
? 'scale-[1.02] border-primary/50 bg-primary/5'
: 'hover:border-muted-foreground/25 hover:bg-muted/20'
)}
>
{isOver ? (
<div className={clsx(
'flex items-center gap-3 px-6 py-3 rounded-md',
'bg-background/90 backdrop-blur-sm',
'border border-primary/30 shadow-lg',
'text-primary font-medium'
)}>
<div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
<div
className={clsx(
'flex items-center gap-3 rounded-md px-6 py-3',
'bg-background/90 backdrop-blur-sm',
'border border-primary/30 shadow-lg',
'font-medium text-primary'
)}
>
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
Отпустите чтобы добавить в группу
</div>
) : (
@@ -267,20 +282,21 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
showEmptyState = false,
}) => {
const [internalIsExpanded, setInternalIsExpanded] = useState(true)
// Используем внешнее состояние если передано, иначе внутреннее
const isExpanded = externalIsExpanded !== undefined ? externalIsExpanded : internalIsExpanded
const isExpanded =
externalIsExpanded !== undefined ? externalIsExpanded : internalIsExpanded
// Отладка: логируем получение шаблонов в группе
console.log(`🏗️ TemplateGroup render for "${group?.name || 'Без группы'}":`, {
groupId: group?.id || 'ungrouped',
templatesCount: templates.length,
templates: templates.map(t => ({
id: t.id,
name: t.name,
templates: templates.map(t => ({
id: t.id,
name: t.name,
order: t.order,
group_id: t.group_id
}))
group_id: t.group_id,
})),
})
const toggleExpanded = useCallback(() => {
@@ -316,14 +332,24 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
const groupId = group?.id || 'ungrouped'
return (
<div className="mb-6">
<div
className={clsx(
'transition-all duration-200',
isExpanded ? 'mb-6' : 'mb-0'
)}
>
{/* Заголовок группы */}
<div className="mb-3 flex items-center justify-between rounded-lg p-4 bg-muted/30 min-h-[60px]">
<div className="flex items-center gap-2 flex-1">
<div
className={clsx(
'flex min-h-[40px] items-center justify-between p-2',
isExpanded ? 'mb-3' : 'mb-0'
)}
>
<div className="flex flex-1 items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 flex-shrink-0"
className="h-6 w-6 flex-shrink-0 p-0"
onClick={toggleExpanded}
>
{isExpanded ? (
@@ -332,21 +358,26 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
<ChevronRight className="h-4 w-4" />
)}
</Button>
{isExpanded ? (
<FolderOpen className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<FolderOpen className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
) : (
<FolderClosed className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<FolderClosed className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
)}
<h3 className="text-lg font-semibold">{groupName}</h3>
<Badge variant="secondary" className={clsx(
"text-xs ml-2",
showEmptyState && templates.length === 0 && "bg-amber-100 text-amber-800 border-amber-200"
)}>
<Badge
variant="secondary"
className={clsx(
'ml-2 text-xs',
showEmptyState &&
templates.length === 0 &&
'border-amber-200 bg-amber-100 text-amber-800'
)}
>
{templates.length}
{showEmptyState && templates.length === 0 && " (отфильтровано)"}
{showEmptyState && templates.length === 0 && ' (отфильтровано)'}
</Badge>
</div>
@@ -405,9 +436,7 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
<div className="space-y-3">
<div className="text-muted-foreground/60">
<p className="text-sm font-medium">Нет результатов</p>
<p className="text-xs">
Попробуйте изменить фильтры
</p>
<p className="text-xs">Попробуйте изменить фильтры</p>
</div>
</div>
</div>
@@ -428,4 +457,4 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
)}
</div>
)
}
}

View File

@@ -6,7 +6,10 @@ import {
TemplateFilters,
} from '@/component/TemplateManager/TemplateFilterSidebar'
import { TemplateGroup } from '@/component/TemplateManager/TemplateGroup'
import { GroupProvider, useGroupContext } from '@/entity/group/model/GroupContext'
import {
GroupProvider,
useGroupContext,
} from '@/entity/group/model/GroupContext'
import {
apiTemplateWithAttributesToTemplate,
getTemplateWithAttributesApi,
@@ -14,7 +17,10 @@ import {
import { useCategoryContext } from '@/entity/template/model/CategoryContext'
import { useTemplateContext } from '@/entity/template/model/TemplateContext'
import { useTemplateOrderState } from '@/hook/useTemplateOrderState'
import { generateInitialOrder, groupTemplatesByGroup } from '@/lib/template-order'
import {
generateInitialOrder,
groupTemplatesByGroup,
} from '@/lib/template-order'
import { Group, Template, TemplateAttributeDetail } from '@/type/template'
import TemplateCard from '@/widget/template/ui/TemplateCard'
import {
@@ -29,10 +35,7 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable'
import { arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import clsx from 'clsx'
import {
CheckSquare,
@@ -55,7 +58,7 @@ import {
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
DialogTitle,
} from 'shared/ui/dialog'
import { Input } from 'shared/ui/input'
import {
@@ -216,14 +219,18 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
className="h-9"
/>
</div>
{/* Селектор группы */}
{showGroupSelector && onGroupChange && (
<div className="space-y-2">
<label className="text-sm font-medium text-muted-foreground">Группа</label>
<label className="text-sm font-medium text-muted-foreground">
Группа
</label>
<Select
value={selectedGroupId || 'no-group'}
onValueChange={(value) => onGroupChange(value === 'no-group' ? null : value)}
onValueChange={value =>
onGroupChange(value === 'no-group' ? null : value)
}
>
<SelectTrigger className="h-9">
<SelectValue placeholder="Выберите группу" />
@@ -232,7 +239,9 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
<SelectItem value="no-group">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-muted-foreground/40" />
<span className="text-muted-foreground">Без группы</span>
<span className="text-muted-foreground">
Без группы
</span>
</div>
</SelectItem>
{groups.map(group => (
@@ -283,11 +292,13 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
<Checkbox
id="copy-first-sheet-empty"
checked={copyFirstSheetEmpty}
onCheckedChange={(checked) => onCopyFirstSheetEmptyChange?.(checked === true)}
onCheckedChange={checked =>
onCopyFirstSheetEmptyChange?.(checked === true)
}
/>
<label
htmlFor="copy-first-sheet-empty"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
className="cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Не копировать левый лист
</label>
@@ -366,7 +377,9 @@ const TemplatesPageContent = () => {
AttributeValue[]
>([])
const [copyFirstSheetEmpty, setCopyFirstSheetEmpty] = useState(false)
const [copySelectedGroupId, setCopySelectedGroupId] = useState<string | null>(null)
const [copySelectedGroupId, setCopySelectedGroupId] = useState<string | null>(
null
)
const [editName, setEditName] = useState('')
const [editDescription, setEditDescription] = useState('')
const [editAttributeValues, setEditAttributeValues] = useState<
@@ -384,10 +397,10 @@ const TemplatesPageContent = () => {
// Состояние для управления порядком шаблонов
const orderState = useTemplateOrderState(templates, groups)
const templatesWithChanges = orderState.getTemplatesWithChanges()
// Состояние для drag and drop
const [activeId, setActiveId] = useState<string | null>(null)
// Настройка сенсоров для @dnd-kit
const sensors = useSensors(
useSensor(PointerSensor, {
@@ -441,13 +454,16 @@ const TemplatesPageContent = () => {
if (newName.trim()) {
try {
// Генерируем order для нового шаблона (в конец группы)
const newOrder = generateInitialOrder(templatesWithChanges, selectedGroupId)
const newOrder = generateInitialOrder(
templatesWithChanges,
selectedGroupId
)
console.log('Creating new template with order:', {
selectedGroupId,
newOrder
newOrder,
})
// Создаем шаблон с атрибутами, группой и order
await addTemplate(
{
@@ -587,136 +603,173 @@ const TemplatesPageContent = () => {
setActiveId(event.active.id as string)
}, [])
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
if (!over || active.id === over.id) {
return
}
const activeTemplate = templatesWithChanges.find(t => t.id === active.id)
if (!activeTemplate) return
// Определяем, куда был сброшен элемент
const overId = over.id as string
console.log('🚀 DRAG END START:', {
activeId: active.id,
overId: overId,
activeTemplate: {
id: activeTemplate.id,
name: activeTemplate.name,
group_id: activeTemplate.group_id,
order: activeTemplate.order
if (!over || active.id === over.id) {
return
}
})
// Проверяем, это drop на left/right зону карточки или на группу
if (overId.startsWith('drop-left-') || overId.startsWith('drop-right-')) {
// Определяем позицию (left/right) и ID карточки
const isLeftDrop = overId.startsWith('drop-left-')
const targetTemplateId = overId.replace(isLeftDrop ? 'drop-left-' : 'drop-right-', '')
const overTemplate = templatesWithChanges.find(t => t.id === targetTemplateId)
if (!overTemplate) return
const sourceGroupId = activeTemplate.group_id
const targetGroupId = overTemplate.group_id
console.log('🎯 Drop on card edge:', {
position: isLeftDrop ? 'left' : 'right',
targetTemplateId,
sourceGroupId,
targetGroupId
const activeTemplate = templatesWithChanges.find(t => t.id === active.id)
if (!activeTemplate) return
// Определяем, куда был сброшен элемент
const overId = over.id as string
console.log('🚀 DRAG END START:', {
activeId: active.id,
overId: overId,
activeTemplate: {
id: activeTemplate.id,
name: activeTemplate.name,
group_id: activeTemplate.group_id,
order: activeTemplate.order,
},
})
if ((sourceGroupId || null) === (targetGroupId || null)) {
// Перетаскивание внутри одной группы
const groupTemplates = templatesWithChanges
.filter(t => (t.group_id || null) === (sourceGroupId || null))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const activeIndex = groupTemplates.findIndex(t => t.id === active.id)
const targetIndex = groupTemplates.findIndex(t => t.id === targetTemplateId)
if (activeIndex !== targetIndex) {
// Определяем позицию вставки: слева (перед) или справа (после)
const insertIndex = isLeftDrop ? targetIndex : targetIndex + 1
const reorderedTemplates = arrayMove(groupTemplates, activeIndex, insertIndex)
// Пересчитываем order для всех элементов в группе
reorderedTemplates.forEach((template, index) => {
const newOrder = (index + 1) * 1000
if (template.order !== newOrder) {
orderState.applyOrderChange(template.id, newOrder, sourceGroupId)
}
})
console.log('🔄 Reordering within group:', {
groupId: sourceGroupId,
activeIndex,
targetIndex,
insertIndex,
position: isLeftDrop ? 'before' : 'after'
})
}
} else {
// Перетаскивание между разными группами
const targetGroupTemplates = templatesWithChanges
.filter(t => (t.group_id || null) === (targetGroupId || null))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const targetIndex = targetGroupTemplates.findIndex(t => t.id === targetTemplateId)
let newOrder: number
if (isLeftDrop) {
// Вставка перед целевой карточкой (слева)
if (targetIndex === 0) {
// Перед первой карточкой
const firstOrder = targetGroupTemplates[0]?.order ?? 1000
newOrder = firstOrder > 1000 ? firstOrder / 2 : firstOrder - 500
} else {
// Между карточками
const prevOrder = targetGroupTemplates[targetIndex - 1]?.order ?? 0
const currentOrder = targetGroupTemplates[targetIndex]?.order ?? 1000
newOrder = (prevOrder + currentOrder) / 2
}
} else {
// Вставка после целевой карточки (справа)
const currentOrder = targetGroupTemplates[targetIndex]?.order ?? 1000
const nextOrder = targetGroupTemplates[targetIndex + 1]?.order ?? (currentOrder + 2000)
newOrder = (currentOrder + nextOrder) / 2
}
console.log('🔀 Moving between groups with edge positioning:', {
// Проверяем, это drop на left/right зону карточки или на группу
if (overId.startsWith('drop-left-') || overId.startsWith('drop-right-')) {
// Определяем позицию (left/right) и ID карточки
const isLeftDrop = overId.startsWith('drop-left-')
const targetTemplateId = overId.replace(
isLeftDrop ? 'drop-left-' : 'drop-right-',
''
)
const overTemplate = templatesWithChanges.find(
t => t.id === targetTemplateId
)
if (!overTemplate) return
const sourceGroupId = activeTemplate.group_id
const targetGroupId = overTemplate.group_id
console.log('🎯 Drop on card edge:', {
position: isLeftDrop ? 'left' : 'right',
targetTemplateId,
sourceGroupId,
targetGroupId,
targetTemplateId,
position: isLeftDrop ? 'before' : 'after',
newOrder
})
orderState.applyOrderChange(active.id as string, newOrder, targetGroupId)
if ((sourceGroupId || null) === (targetGroupId || null)) {
// Перетаскивание внутри одной группы
const groupTemplates = templatesWithChanges
.filter(t => (t.group_id || null) === (sourceGroupId || null))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const activeIndex = groupTemplates.findIndex(t => t.id === active.id)
const targetIndex = groupTemplates.findIndex(
t => t.id === targetTemplateId
)
if (activeIndex !== targetIndex) {
// Определяем позицию вставки: слева (перед) или справа (после)
const insertIndex = isLeftDrop ? targetIndex : targetIndex + 1
const reorderedTemplates = arrayMove(
groupTemplates,
activeIndex,
insertIndex
)
// Пересчитываем order для всех элементов в группе
reorderedTemplates.forEach((template, index) => {
const newOrder = (index + 1) * 1000
if (template.order !== newOrder) {
orderState.applyOrderChange(
template.id,
newOrder,
sourceGroupId
)
}
})
console.log('🔄 Reordering within group:', {
groupId: sourceGroupId,
activeIndex,
targetIndex,
insertIndex,
position: isLeftDrop ? 'before' : 'after',
})
}
} else {
// Перетаскивание между разными группами
const targetGroupTemplates = templatesWithChanges
.filter(t => (t.group_id || null) === (targetGroupId || null))
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
const targetIndex = targetGroupTemplates.findIndex(
t => t.id === targetTemplateId
)
let newOrder: number
if (isLeftDrop) {
// Вставка перед целевой карточкой (слева)
if (targetIndex === 0) {
// Перед первой карточкой
const firstOrder = targetGroupTemplates[0]?.order ?? 1000
newOrder = firstOrder > 1000 ? firstOrder / 2 : firstOrder - 500
} else {
// Между карточками
const prevOrder =
targetGroupTemplates[targetIndex - 1]?.order ?? 0
const currentOrder =
targetGroupTemplates[targetIndex]?.order ?? 1000
newOrder = (prevOrder + currentOrder) / 2
}
} else {
// Вставка после целевой карточки (справа)
const currentOrder =
targetGroupTemplates[targetIndex]?.order ?? 1000
const nextOrder =
targetGroupTemplates[targetIndex + 1]?.order ??
currentOrder + 2000
newOrder = (currentOrder + nextOrder) / 2
}
console.log('🔀 Moving between groups with edge positioning:', {
sourceGroupId,
targetGroupId,
targetTemplateId,
position: isLeftDrop ? 'before' : 'after',
newOrder,
})
orderState.applyOrderChange(
active.id as string,
newOrder,
targetGroupId
)
}
} else {
// Сброс на пустую группу
const isOverGroup =
groups.some(g => g.id === overId) || overId === 'ungrouped'
if (isOverGroup) {
const targetGroupId = overId === 'ungrouped' ? null : overId
const newOrder = generateInitialOrder(
templatesWithChanges,
targetGroupId
)
console.log('🗂️ Moving to empty group:', {
targetGroupId,
newOrder,
})
orderState.applyOrderChange(
active.id as string,
newOrder,
targetGroupId
)
}
}
} else {
// Сброс на пустую группу
const isOverGroup = groups.some(g => g.id === overId) || overId === 'ungrouped'
if (isOverGroup) {
const targetGroupId = overId === 'ungrouped' ? null : overId
const newOrder = generateInitialOrder(templatesWithChanges, targetGroupId)
console.log('🗂️ Moving to empty group:', {
targetGroupId,
newOrder
})
orderState.applyOrderChange(active.id as string, newOrder, targetGroupId)
}
}
}, [templatesWithChanges, orderState, groups])
},
[templatesWithChanges, orderState, groups]
)
const handleDragOver = useCallback((event: DragOverEvent) => {
// Можно добавить дополнительную логику если нужно
@@ -724,7 +777,11 @@ const TemplatesPageContent = () => {
// Проверяем есть ли активные фильтры
const hasActiveFilters = useMemo(() => {
return !!(filters.name || filters.description || Object.keys(filters.attributes).length > 0)
return !!(
filters.name ||
filters.description ||
Object.keys(filters.attributes).length > 0
)
}, [filters])
// Фильтрация шаблонов
@@ -790,24 +847,31 @@ const TemplatesPageContent = () => {
// Группировка шаблонов по группам с правильной сортировкой
const groupedTemplates = useMemo(() => {
console.log('🗂️ Grouping templates, input:',
filteredTemplates.map(t => ({ id: t.id, name: t.name, group_id: t.group_id, order: t.order }))
console.log(
'🗂️ Grouping templates, input:',
filteredTemplates.map(t => ({
id: t.id,
name: t.name,
group_id: t.group_id,
order: t.order,
}))
)
const grouped = groupTemplatesByGroup(filteredTemplates)
console.log('🗂️ Grouped templates (before sorting):',
console.log(
'🗂️ Grouped templates (before sorting):',
Object.fromEntries(
Object.entries(grouped).map(([key, templates]) => [
key,
templates.map(t => ({ id: t.id, name: t.name, order: t.order }))
key,
templates.map(t => ({ id: t.id, name: t.name, order: t.order })),
])
)
)
// Сортируем шаблоны в каждой группе по порядку
const sortedGrouped: typeof grouped = {}
Object.keys(grouped).forEach(groupKey => {
sortedGrouped[groupKey] = grouped[groupKey].sort((a, b) => {
// Правильная обработка null/undefined порядков
@@ -816,16 +880,17 @@ const TemplatesPageContent = () => {
return orderA - orderB
})
})
console.log('🗂️ Grouped templates (after sorting):',
console.log(
'🗂️ Grouped templates (after sorting):',
Object.fromEntries(
Object.entries(sortedGrouped).map(([key, templates]) => [
key,
templates.map(t => ({ id: t.id, name: t.name, order: t.order }))
key,
templates.map(t => ({ id: t.id, name: t.name, order: t.order })),
])
)
)
return sortedGrouped
}, [filteredTemplates])
@@ -837,9 +902,13 @@ const TemplatesPageContent = () => {
const totalTemplates = templatesWithChanges.length
const filteredCount = filteredTemplates.length
const filteredGroups = new Set(filteredTemplates.map(t => t.group_id || null))
const totalGroups = new Set(templatesWithChanges.map(t => t.group_id || null))
const filteredGroups = new Set(
filteredTemplates.map(t => t.group_id || null)
)
const totalGroups = new Set(
templatesWithChanges.map(t => t.group_id || null)
)
return {
totalTemplates,
filteredTemplates: filteredCount,
@@ -873,7 +942,12 @@ const TemplatesPageContent = () => {
hasUnsavedChanges={orderState.hasUnsavedChanges}
isSaving={orderState.isSaving}
lastSaveError={orderState.lastSaveError}
onSave={() => orderState.saveOrderChanges(updateTemplateOrder, moveTemplateToGroup)}
onSave={() =>
orderState.saveOrderChanges(
updateTemplateOrder,
moveTemplateToGroup
)
}
onClearError={orderState.clearError}
changedCount={orderState.changedCount}
/>
@@ -903,11 +977,11 @@ const TemplatesPageContent = () => {
{!selectionMode && (
<>
<GroupManager
<GroupManager
editingGroupId={editingGroupId}
onEditingGroupChange={setEditingGroupId}
/>
<Button
size="sm"
variant="outline"
@@ -1022,13 +1096,20 @@ const TemplatesPageContent = () => {
{filterStats && (
<div className="mb-4 rounded-lg bg-muted/50 p-3">
<div className="text-sm text-muted-foreground">
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
Показано <span className="font-medium text-foreground">{filterStats.filteredTemplates}</span> из {filterStats.totalTemplates} шаблонов
Показано{' '}
<span className="font-medium text-foreground">
{filterStats.filteredTemplates}
</span>{' '}
из {filterStats.totalTemplates} шаблонов
</div>
<div className="flex items-center gap-4 text-xs">
<span>
Групп с результатами: <span className="font-medium text-foreground">{filterStats.groupsWithResults}</span>
Групп с результатами:{' '}
<span className="font-medium text-foreground">
{filterStats.groupsWithResults}
</span>
</span>
{filterStats.emptyGroups > 0 && (
<span className="text-amber-600">
@@ -1039,7 +1120,8 @@ const TemplatesPageContent = () => {
</div>
{Object.keys(filters.attributes).length > 0 && (
<div className="mt-1 text-xs">
Активных фильтров по атрибутам: {Object.keys(filters.attributes).length}
Активных фильтров по атрибутам:{' '}
{Object.keys(filters.attributes).length}
</div>
)}
</div>
@@ -1055,17 +1137,22 @@ const TemplatesPageContent = () => {
onDragOver={handleDragOver}
>
{/* Отображение групп шаблонов */}
<div className={clsx(
"space-y-6 transition-opacity duration-200",
activeId && "opacity-90"
)}>
<div
className={clsx(
'transition-opacity duration-200',
activeId && 'opacity-90'
)}
>
{/* Все группы - показываем все группы при фильтрации для сохранения контекста */}
{sortedGroups.map(group => {
const groupTemplates = groupedTemplates[group.id] || []
const showGroup = !hasActiveFilters || groupTemplates.length > 0 || hasActiveFilters
const showGroup =
!hasActiveFilters ||
groupTemplates.length > 0 ||
hasActiveFilters
if (!showGroup) return null
return (
<TemplateGroup
key={group.id}
@@ -1081,9 +1168,13 @@ const TemplatesPageContent = () => {
onDeleteGroup={handleDeleteGroup}
activeFilters={filters.attributes}
isExpanded={orderState.getGroupExpanded(group.id)}
onToggleExpanded={() => orderState.toggleGroupExpanded(group.id)}
onToggleExpanded={() =>
orderState.toggleGroupExpanded(group.id)
}
onCreateTemplate={handleCreateTemplate}
showEmptyState={hasActiveFilters && groupTemplates.length === 0}
showEmptyState={
hasActiveFilters && groupTemplates.length === 0
}
/>
)
})}
@@ -1102,48 +1193,63 @@ const TemplatesPageContent = () => {
isExpanded={orderState.getGroupExpanded(null)}
onToggleExpanded={() => orderState.toggleGroupExpanded(null)}
onCreateTemplate={handleCreateTemplate}
showEmptyState={hasActiveFilters && (!groupedTemplates.ungrouped || groupedTemplates.ungrouped.length === 0)}
showEmptyState={
hasActiveFilters &&
(!groupedTemplates.ungrouped ||
groupedTemplates.ungrouped.length === 0)
}
/>
{/* Если нет групп и нет шаблонов вообще, показываем кнопку создания */}
{sortedGroups.length === 0 && templatesWithChanges.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-muted-foreground mb-6">
<p className="text-lg font-medium">Нет групп и шаблонов</p>
<p className="text-sm">Создайте группу или шаблон для начала работы</p>
{sortedGroups.length === 0 &&
templatesWithChanges.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="mb-6 text-muted-foreground">
<p className="text-lg font-medium">
Нет групп и шаблонов
</p>
<p className="text-sm">
Создайте группу или шаблон для начала работы
</p>
</div>
<div className="flex gap-3">
<Button
onClick={() => handleCreateTemplate()}
className="gap-2"
>
<Plus className="h-4 w-4" />
Создать шаблон
</Button>
</div>
</div>
<div className="flex gap-3">
<Button
onClick={() => handleCreateTemplate()}
className="gap-2"
>
<Plus className="h-4 w-4" />
Создать шаблон
</Button>
</div>
</div>
)}
)}
</div>
{/* DragOverlay для красивого перетаскивания */}
<DragOverlay>
{activeId ? (
<div className={clsx(
'transform transition-transform duration-200',
'rotate-2 scale-105 opacity-90',
'drop-shadow-2xl shadow-2xl',
'pointer-events-none z-[9999]'
)}>
<div className={clsx(
'rounded-lg overflow-hidden',
'bg-background border-0',
// Полностью убираем все borders из вложенных элементов
'[&_*]:!border-0 [&_*]:!border-transparent [&_*]:!ring-0',
// Добавляем собственную тень и border
'ring-2 ring-primary/20 shadow-primary/10'
)}>
<div
className={clsx(
'transform transition-transform duration-200',
'rotate-2 scale-105 opacity-90',
'shadow-2xl drop-shadow-2xl',
'pointer-events-none z-[9999]'
)}
>
<div
className={clsx(
'overflow-hidden rounded-lg',
'border-0 bg-background',
// Полностью убираем все borders из вложенных элементов
'[&_*]:!border-0 [&_*]:!border-transparent [&_*]:!ring-0',
// Добавляем собственную тень и border
'shadow-primary/10 ring-2 ring-primary/20'
)}
>
<TemplateCard
template={templatesWithChanges.find(t => t.id === activeId)!}
template={
templatesWithChanges.find(t => t.id === activeId)!
}
selectionMode={false}
isSelected={false}
activeFilters={filters.attributes}
@@ -1154,24 +1260,28 @@ const TemplatesPageContent = () => {
</DragOverlay>
</DndContext>
{filteredTemplates.length === 0 && templates.length > 0 && hasActiveFilters && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-muted-foreground mb-6">
<p className="text-lg font-medium">Шаблоны не найдены</p>
<p className="text-sm">Ни один шаблон не соответствует заданным фильтрам</p>
{filteredTemplates.length === 0 &&
templates.length > 0 &&
hasActiveFilters && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="mb-6 text-muted-foreground">
<p className="text-lg font-medium">Шаблоны не найдены</p>
<p className="text-sm">
Ни один шаблон не соответствует заданным фильтрам
</p>
</div>
<Button
variant="outline"
onClick={() =>
setFilters({ name: '', description: '', attributes: {} })
}
className="gap-2"
>
<X className="h-4 w-4" />
Сбросить все фильтры
</Button>
</div>
<Button
variant="outline"
onClick={() => setFilters({ name: '', description: '', attributes: {} })}
className="gap-2"
>
<X className="h-4 w-4" />
Сбросить все фильтры
</Button>
</div>
)}
)}
</main>
</SidebarInset>
</SidebarProvider>