дизайн групп
This commit is contained in:
@@ -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',
|
||||
<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',
|
||||
<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,
|
||||
}
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
@@ -179,11 +190,11 @@ const DroppableGroup: React.FC<DroppableGroupProps> = ({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={clsx(
|
||||
'transition-colors duration-200 relative',
|
||||
'relative transition-colors duration-200',
|
||||
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'
|
||||
? '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',
|
||||
<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
|
||||
? 'border-primary/50 bg-primary/5 scale-[1.02]'
|
||||
? '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',
|
||||
<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',
|
||||
'text-primary font-medium'
|
||||
)}>
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
|
||||
'font-medium text-primary'
|
||||
)}
|
||||
>
|
||||
<div className="h-2 w-2 animate-pulse rounded-full bg-primary" />
|
||||
Отпустите чтобы добавить в группу
|
||||
</div>
|
||||
) : (
|
||||
@@ -269,7 +284,8 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
|
||||
const [internalIsExpanded, setInternalIsExpanded] = useState(true)
|
||||
|
||||
// Используем внешнее состояние если передано, иначе внутреннее
|
||||
const isExpanded = externalIsExpanded !== undefined ? externalIsExpanded : internalIsExpanded
|
||||
const isExpanded =
|
||||
externalIsExpanded !== undefined ? externalIsExpanded : internalIsExpanded
|
||||
|
||||
// Отладка: логируем получение шаблонов в группе
|
||||
console.log(`🏗️ TemplateGroup render for "${group?.name || 'Без группы'}":`, {
|
||||
@@ -279,8 +295,8 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
|
||||
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 ? (
|
||||
@@ -334,19 +360,24 @@ export const TemplateGroup: React.FC<TemplateGroupProps> = ({
|
||||
</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>
|
||||
|
||||
@@ -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 {
|
||||
@@ -220,10 +223,14 @@ const TemplateForm: React.FC<TemplateFormProps> = ({
|
||||
{/* Селектор группы */}
|
||||
{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<
|
||||
@@ -441,11 +454,14 @@ 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
|
||||
@@ -587,7 +603,8 @@ const TemplatesPageContent = () => {
|
||||
setActiveId(event.active.id as string)
|
||||
}, [])
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
|
||||
@@ -608,16 +625,21 @@ const TemplatesPageContent = () => {
|
||||
id: activeTemplate.id,
|
||||
name: activeTemplate.name,
|
||||
group_id: activeTemplate.group_id,
|
||||
order: activeTemplate.order
|
||||
}
|
||||
order: activeTemplate.order,
|
||||
},
|
||||
})
|
||||
|
||||
// Проверяем, это 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)
|
||||
const targetTemplateId = overId.replace(
|
||||
isLeftDrop ? 'drop-left-' : 'drop-right-',
|
||||
''
|
||||
)
|
||||
const overTemplate = templatesWithChanges.find(
|
||||
t => t.id === targetTemplateId
|
||||
)
|
||||
|
||||
if (!overTemplate) return
|
||||
|
||||
@@ -628,7 +650,7 @@ const TemplatesPageContent = () => {
|
||||
position: isLeftDrop ? 'left' : 'right',
|
||||
targetTemplateId,
|
||||
sourceGroupId,
|
||||
targetGroupId
|
||||
targetGroupId,
|
||||
})
|
||||
|
||||
if ((sourceGroupId || null) === (targetGroupId || null)) {
|
||||
@@ -638,18 +660,28 @@ const TemplatesPageContent = () => {
|
||||
.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)
|
||||
const targetIndex = groupTemplates.findIndex(
|
||||
t => t.id === targetTemplateId
|
||||
)
|
||||
|
||||
if (activeIndex !== targetIndex) {
|
||||
// Определяем позицию вставки: слева (перед) или справа (после)
|
||||
const insertIndex = isLeftDrop ? targetIndex : targetIndex + 1
|
||||
const reorderedTemplates = arrayMove(groupTemplates, activeIndex, insertIndex)
|
||||
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)
|
||||
orderState.applyOrderChange(
|
||||
template.id,
|
||||
newOrder,
|
||||
sourceGroupId
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -658,7 +690,7 @@ const TemplatesPageContent = () => {
|
||||
activeIndex,
|
||||
targetIndex,
|
||||
insertIndex,
|
||||
position: isLeftDrop ? 'before' : 'after'
|
||||
position: isLeftDrop ? 'before' : 'after',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -667,7 +699,9 @@ const TemplatesPageContent = () => {
|
||||
.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)
|
||||
const targetIndex = targetGroupTemplates.findIndex(
|
||||
t => t.id === targetTemplateId
|
||||
)
|
||||
|
||||
let newOrder: number
|
||||
|
||||
@@ -679,14 +713,19 @@ const TemplatesPageContent = () => {
|
||||
newOrder = firstOrder > 1000 ? firstOrder / 2 : firstOrder - 500
|
||||
} else {
|
||||
// Между карточками
|
||||
const prevOrder = targetGroupTemplates[targetIndex - 1]?.order ?? 0
|
||||
const currentOrder = targetGroupTemplates[targetIndex]?.order ?? 1000
|
||||
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)
|
||||
const currentOrder =
|
||||
targetGroupTemplates[targetIndex]?.order ?? 1000
|
||||
const nextOrder =
|
||||
targetGroupTemplates[targetIndex + 1]?.order ??
|
||||
currentOrder + 2000
|
||||
newOrder = (currentOrder + nextOrder) / 2
|
||||
}
|
||||
|
||||
@@ -695,28 +734,42 @@ const TemplatesPageContent = () => {
|
||||
targetGroupId,
|
||||
targetTemplateId,
|
||||
position: isLeftDrop ? 'before' : 'after',
|
||||
newOrder
|
||||
newOrder,
|
||||
})
|
||||
|
||||
orderState.applyOrderChange(active.id as string, newOrder, targetGroupId)
|
||||
orderState.applyOrderChange(
|
||||
active.id as string,
|
||||
newOrder,
|
||||
targetGroupId
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Сброс на пустую группу
|
||||
const isOverGroup = groups.some(g => g.id === overId) || overId === 'ungrouped'
|
||||
const isOverGroup =
|
||||
groups.some(g => g.id === overId) || overId === 'ungrouped'
|
||||
|
||||
if (isOverGroup) {
|
||||
const targetGroupId = overId === 'ungrouped' ? null : overId
|
||||
const newOrder = generateInitialOrder(templatesWithChanges, targetGroupId)
|
||||
const newOrder = generateInitialOrder(
|
||||
templatesWithChanges,
|
||||
targetGroupId
|
||||
)
|
||||
|
||||
console.log('🗂️ Moving to empty group:', {
|
||||
targetGroupId,
|
||||
newOrder
|
||||
newOrder,
|
||||
})
|
||||
|
||||
orderState.applyOrderChange(active.id as string, newOrder, targetGroupId)
|
||||
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,17 +847,24 @@ 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 }))
|
||||
templates.map(t => ({ id: t.id, name: t.name, order: t.order })),
|
||||
])
|
||||
)
|
||||
)
|
||||
@@ -817,11 +881,12 @@ const TemplatesPageContent = () => {
|
||||
})
|
||||
})
|
||||
|
||||
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 }))
|
||||
templates.map(t => ({ id: t.id, name: t.name, order: t.order })),
|
||||
])
|
||||
)
|
||||
)
|
||||
@@ -837,8 +902,12 @@ 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,
|
||||
@@ -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}
|
||||
/>
|
||||
@@ -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,14 +1137,19 @@ 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
|
||||
|
||||
@@ -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,15 +1193,24 @@ 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 && (
|
||||
{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>
|
||||
<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
|
||||
@@ -1128,22 +1228,28 @@ const TemplatesPageContent = () => {
|
||||
{/* DragOverlay для красивого перетаскивания */}
|
||||
<DragOverlay>
|
||||
{activeId ? (
|
||||
<div className={clsx(
|
||||
<div
|
||||
className={clsx(
|
||||
'transform transition-transform duration-200',
|
||||
'rotate-2 scale-105 opacity-90',
|
||||
'drop-shadow-2xl shadow-2xl',
|
||||
'shadow-2xl drop-shadow-2xl',
|
||||
'pointer-events-none z-[9999]'
|
||||
)}>
|
||||
<div className={clsx(
|
||||
'rounded-lg overflow-hidden',
|
||||
'bg-background border-0',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden rounded-lg',
|
||||
'border-0 bg-background',
|
||||
// Полностью убираем все borders из вложенных элементов
|
||||
'[&_*]:!border-0 [&_*]:!border-transparent [&_*]:!ring-0',
|
||||
// Добавляем собственную тень и border
|
||||
'ring-2 ring-primary/20 shadow-primary/10'
|
||||
)}>
|
||||
'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,15 +1260,21 @@ const TemplatesPageContent = () => {
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
|
||||
{filteredTemplates.length === 0 && templates.length > 0 && hasActiveFilters && (
|
||||
{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">
|
||||
<div className="mb-6 text-muted-foreground">
|
||||
<p className="text-lg font-medium">Шаблоны не найдены</p>
|
||||
<p className="text-sm">Ни один шаблон не соответствует заданным фильтрам</p>
|
||||
<p className="text-sm">
|
||||
Ни один шаблон не соответствует заданным фильтрам
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setFilters({ name: '', description: '', attributes: {} })}
|
||||
onClick={() =>
|
||||
setFilters({ name: '', description: '', attributes: {} })
|
||||
}
|
||||
className="gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
@@ -1170,8 +1282,6 @@ const TemplatesPageContent = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
Reference in New Issue
Block a user