дизайн групп

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

View File

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