fix(ui): bulk-панель выделения заменяет тулбар на месте — список больше не сдвигается

- Home: панель действий рендерится вместо строки поиска (те же метрики: py-1, h-8 md:h-6)
- Реестр: панель «N выбрано / Удалить» внутри тулбара TaskRegistry (swap содержимого строки)
This commit is contained in:
2026-09-04 14:42:53 +03:00
parent 49209ddacd
commit 013e97c8d3
3 changed files with 44 additions and 36 deletions

View File

@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Settings2, Calendar, User, Search, X as XIcon, Paperclip, AlertCircle, CheckCircle2, GripVertical } from 'lucide-react';
import { Settings2, Calendar, User, Search, X as XIcon, Paperclip, AlertCircle, CheckCircle2, GripVertical, Trash2 } from 'lucide-react';
import { type ColumnSort } from '@/hooks/useColumnFilters';
import { resolveFieldDisplayValue, type FieldDisplayContext } from '@/lib/fieldDisplay';
import { statusBadgeStyle } from '@/lib/statusColor';
@@ -121,6 +121,9 @@ interface TaskRegistryProps {
onToggleSelectAll?: () => void;
onSelectRange?: (taskIds: number[]) => void;
onSetAnchorTaskId?: (taskId: number | null) => void;
// Bulk actions (тулбар заменяется панелью на месте — без сдвига layout)
onClearSelection?: () => void;
onRequestBulkDelete?: () => void;
}
// Отображает значение поля с учётом типа
@@ -258,7 +261,7 @@ const REGISTRY_INLINE_TYPES = new Set([
// Типы полей, которые переключаются по одиночному клику
const REGISTRY_QUICK_TOGGLE_TYPES = new Set(['checkbox', 'toggle']);
const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = [], roles = [], formId, onFilteredCountChange, onTaskClick, filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters, searches, onAddSearch, onRemoveSearch, isAdmin, selectedTaskIds, anchorTaskId, onToggleSelected, onToggleSelectAll, onSelectRange, onSetAnchorTaskId }: TaskRegistryProps) => {
const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users = [], roles = [], formId, onFilteredCountChange, onTaskClick, filters, sort, setFilter, clearFilter, clearAll, setSort, clearSort, hasActiveFilters, searches, onAddSearch, onRemoveSearch, isAdmin, selectedTaskIds, anchorTaskId, onToggleSelected, onToggleSelectAll, onSelectRange, onSetAnchorTaskId, onClearSelection, onRequestBulkDelete }: TaskRegistryProps) => {
const { toast } = useToast();
// Текущая редактируемая ячейка: { taskId, columnKey }
@@ -683,8 +686,30 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
return (
<div className="h-full flex flex-col">
{/* Панель управления + активные фильтры (один ряд) */}
{/* Панель управления + активные фильтры (один ряд); при выделении — bulk-панель на том же месте */}
<div className="flex items-center gap-2 mb-1">
{isAdmin && selectedTaskIds && selectedTaskIds.size > 0 && onRequestBulkDelete ? (
<>
<span className="text-xs font-medium text-foreground">{selectedTaskIds.size} выбрано</span>
<Button
variant="outline"
size="sm"
className="h-8 md:h-6 text-[11px] gap-1 px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={onRequestBulkDelete}
>
<Trash2 className="w-3 h-3" />Удалить
</Button>
<div className="flex-1" />
<button
className="text-muted-foreground hover:text-foreground p-1 rounded"
onClick={onClearSelection}
title="Снять выделение"
>
<XIcon className="w-3.5 h-3.5" />
</button>
</>
) : (
<>
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 md:h-6 md:w-6 shrink-0 rounded-full" title="Настроить колонки" data-testid="button-column-settings">
@@ -781,6 +806,8 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
className="py-0"
/>
)}
</>
)}
</div>
{/* Таблица задач */}

View File

@@ -1269,13 +1269,13 @@ export default function HomePage() {
const tabContent = (
<>
{/* Bulk actions toolbar */}
{selectedCount > 0 && (
<div className="flex items-center gap-2 flex-wrap px-3 py-1.5 border-b border-border bg-muted/30 shrink-0">
{/* Bulk actions toolbar swaps with the search row in place — no layout shift */}
{selectedCount > 0 ? (
<div className="flex items-center gap-2 flex-wrap px-3 py-1 border-b border-border bg-muted/30 shrink-0">
<span className="text-xs font-medium text-foreground">{selectedCount} выбрано</span>
{canBulkRead && (
<Button
variant="outline" size="sm" className="h-6 text-[11px] gap-1 px-2"
variant="outline" size="sm" className="h-8 md:h-6 text-[11px] gap-1 px-2"
onClick={() => bulkReadMutation.mutate(Array.from(selectedTaskIds))}
disabled={bulkReadMutation.isPending}
>
@@ -1284,21 +1284,21 @@ export default function HomePage() {
)}
{canBulkSnooze && (
<Button
variant="outline" size="sm" className="h-6 text-[11px] gap-1 px-2"
variant="outline" size="sm" className="h-8 md:h-6 text-[11px] gap-1 px-2"
onClick={() => setSnoozeDialogOpen(true)}
>
<Clock className="w-3 h-3" />Отложить
</Button>
)}
<Button
variant="outline" size="sm" className="h-6 text-[11px] gap-1 px-2"
variant="outline" size="sm" className="h-8 md:h-6 text-[11px] gap-1 px-2"
onClick={() => { setStatusDialogOpen(true); setSelectedTransitionId(null); }}
>
<ArrowRightLeft className="w-3 h-3" />Статус
</Button>
{canBulkHide && (
<Button
variant="outline" size="sm" className="h-6 text-[11px] gap-1 px-2"
variant="outline" size="sm" className="h-8 md:h-6 text-[11px] gap-1 px-2"
onClick={() => bulkHideMutation.mutate(Array.from(selectedTaskIds))}
disabled={bulkHideMutation.isPending}
>
@@ -1307,7 +1307,7 @@ export default function HomePage() {
)}
{canBulkUnhide && (
<Button
variant="outline" size="sm" className="h-6 text-[11px] gap-1 px-2"
variant="outline" size="sm" className="h-8 md:h-6 text-[11px] gap-1 px-2"
onClick={() => bulkUnhideMutation.mutate(Array.from(selectedTaskIds))}
disabled={bulkUnhideMutation.isPending}
>
@@ -1323,9 +1323,8 @@ export default function HomePage() {
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
{/* Search + filter badges — one row, like TaskRegistry */}
) : (
/* Search + filter badges — one row, like TaskRegistry */
<div className="flex items-center gap-2 flex-wrap px-3 py-1 border-b border-border shrink-0">
{/* Grouping settings button — compact rounded-full, like TaskRegistry column settings */}
{(tab === 'inbox' || tab === 'snoozed' || tab === 'hidden') && (
@@ -1375,6 +1374,7 @@ export default function HomePage() {
onRemoveFilter={clearFilter} onRemoveSort={clearSort} onClearAll={clearAll} />
)}
</div>
)}
{!isOnline && tab === 'inbox' && !isUsingCache && !currentQuery.data && !currentQuery.isLoading && (
<div className="flex items-center gap-2 px-4 py-2 bg-warning text-warning-foreground border-b border-warning-foreground/20 shrink-0">

View File

@@ -9,7 +9,7 @@ import { Sidebar } from '@/components/Sidebar';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ArrowLeft, Grid, Table, Plus, Link2, AlertCircle, Trash2, X } from 'lucide-react';
import { ArrowLeft, Grid, Table, Plus, Link2, AlertCircle } from 'lucide-react';
import { Link } from 'wouter';
import TaskRegistry from '../components/TaskRegistry';
import { useColumnFilters } from '@/hooks/useColumnFilters';
@@ -479,27 +479,6 @@ const TaskView = () => {
</div>
<TabsContent value="registry" className="flex-1 mt-0 px-2 pt-1 pb-0 overflow-hidden flex flex-col">
{isAdmin(currentUser) && selectedTaskIds.size > 0 && (
<div className="flex items-center gap-2 flex-wrap px-2 py-1.5 border-b border-border bg-muted/30 shrink-0 mb-1">
<span className="text-xs font-medium text-foreground">{selectedTaskIds.size} выбрано</span>
<Button
variant="outline"
size="sm"
className="h-6 text-[11px] gap-1 px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={() => setDeleteDialogOpen(true)}
>
<Trash2 className="w-3 h-3" />Удалить
</Button>
<div className="flex-1" />
<button
className="text-muted-foreground hover:text-foreground p-1 rounded"
onClick={clearSelection}
title="Снять выделение"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
<div className="flex-1 overflow-auto min-h-0">
<TaskRegistry
tasks={tasks}
@@ -531,6 +510,8 @@ const TaskView = () => {
onToggleSelectAll={selectAllVisible}
onSelectRange={selectRange}
onSetAnchorTaskId={setAnchorTaskId}
onClearSelection={clearSelection}
onRequestBulkDelete={() => setDeleteDialogOpen(true)}
/>
</div>
</TabsContent>