fix(ui): bulk-панель выделения заменяет тулбар на месте — список больше не сдвигается
- Home: панель действий рендерится вместо строки поиска (те же метрики: py-1, h-8 md:h-6) - Реестр: панель «N выбрано / Удалить» внутри тулбара TaskRegistry (swap содержимого строки)
This commit is contained in:
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { type ColumnSort } from '@/hooks/useColumnFilters';
|
||||||
import { resolveFieldDisplayValue, type FieldDisplayContext } from '@/lib/fieldDisplay';
|
import { resolveFieldDisplayValue, type FieldDisplayContext } from '@/lib/fieldDisplay';
|
||||||
import { statusBadgeStyle } from '@/lib/statusColor';
|
import { statusBadgeStyle } from '@/lib/statusColor';
|
||||||
@@ -121,6 +121,9 @@ interface TaskRegistryProps {
|
|||||||
onToggleSelectAll?: () => void;
|
onToggleSelectAll?: () => void;
|
||||||
onSelectRange?: (taskIds: number[]) => void;
|
onSelectRange?: (taskIds: number[]) => void;
|
||||||
onSetAnchorTaskId?: (taskId: number | null) => 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 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();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Текущая редактируемая ячейка: { taskId, columnKey }
|
// Текущая редактируемая ячейка: { taskId, columnKey }
|
||||||
@@ -683,8 +686,30 @@ const TaskRegistry = ({ tasks, relatedTaskTitles = {}, fields, statuses, users =
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{/* Панель управления + активные фильтры (один ряд) */}
|
{/* Панель управления + активные фильтры (один ряд); при выделении — bulk-панель на том же месте */}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<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>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<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">
|
<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"
|
className="py-0"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Таблица задач */}
|
{/* Таблица задач */}
|
||||||
|
|||||||
@@ -1269,13 +1269,13 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const tabContent = (
|
const tabContent = (
|
||||||
<>
|
<>
|
||||||
{/* Bulk actions toolbar */}
|
{/* Bulk actions toolbar swaps with the search row in place — no layout shift */}
|
||||||
{selectedCount > 0 && (
|
{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">
|
<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>
|
<span className="text-xs font-medium text-foreground">{selectedCount} выбрано</span>
|
||||||
{canBulkRead && (
|
{canBulkRead && (
|
||||||
<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={() => bulkReadMutation.mutate(Array.from(selectedTaskIds))}
|
onClick={() => bulkReadMutation.mutate(Array.from(selectedTaskIds))}
|
||||||
disabled={bulkReadMutation.isPending}
|
disabled={bulkReadMutation.isPending}
|
||||||
>
|
>
|
||||||
@@ -1284,21 +1284,21 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
{canBulkSnooze && (
|
{canBulkSnooze && (
|
||||||
<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={() => setSnoozeDialogOpen(true)}
|
onClick={() => setSnoozeDialogOpen(true)}
|
||||||
>
|
>
|
||||||
<Clock className="w-3 h-3" />Отложить
|
<Clock className="w-3 h-3" />Отложить
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<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); }}
|
onClick={() => { setStatusDialogOpen(true); setSelectedTransitionId(null); }}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft className="w-3 h-3" />Статус
|
<ArrowRightLeft className="w-3 h-3" />Статус
|
||||||
</Button>
|
</Button>
|
||||||
{canBulkHide && (
|
{canBulkHide && (
|
||||||
<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={() => bulkHideMutation.mutate(Array.from(selectedTaskIds))}
|
onClick={() => bulkHideMutation.mutate(Array.from(selectedTaskIds))}
|
||||||
disabled={bulkHideMutation.isPending}
|
disabled={bulkHideMutation.isPending}
|
||||||
>
|
>
|
||||||
@@ -1307,7 +1307,7 @@ export default function HomePage() {
|
|||||||
)}
|
)}
|
||||||
{canBulkUnhide && (
|
{canBulkUnhide && (
|
||||||
<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={() => bulkUnhideMutation.mutate(Array.from(selectedTaskIds))}
|
onClick={() => bulkUnhideMutation.mutate(Array.from(selectedTaskIds))}
|
||||||
disabled={bulkUnhideMutation.isPending}
|
disabled={bulkUnhideMutation.isPending}
|
||||||
>
|
>
|
||||||
@@ -1323,9 +1323,8 @@ export default function HomePage() {
|
|||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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 */}
|
{/* Grouping settings button — compact rounded-full, like TaskRegistry column settings */}
|
||||||
{(tab === 'inbox' || tab === 'snoozed' || tab === 'hidden') && (
|
{(tab === 'inbox' || tab === 'snoozed' || tab === 'hidden') && (
|
||||||
@@ -1375,6 +1374,7 @@ export default function HomePage() {
|
|||||||
onRemoveFilter={clearFilter} onRemoveSort={clearSort} onClearAll={clearAll} />
|
onRemoveFilter={clearFilter} onRemoveSort={clearSort} onClearAll={clearAll} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isOnline && tab === 'inbox' && !isUsingCache && !currentQuery.data && !currentQuery.isLoading && (
|
{!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">
|
<div className="flex items-center gap-2 px-4 py-2 bg-warning text-warning-foreground border-b border-warning-foreground/20 shrink-0">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Sidebar } from '@/components/Sidebar';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
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 { Link } from 'wouter';
|
||||||
import TaskRegistry from '../components/TaskRegistry';
|
import TaskRegistry from '../components/TaskRegistry';
|
||||||
import { useColumnFilters } from '@/hooks/useColumnFilters';
|
import { useColumnFilters } from '@/hooks/useColumnFilters';
|
||||||
@@ -479,27 +479,6 @@ const TaskView = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="registry" className="flex-1 mt-0 px-2 pt-1 pb-0 overflow-hidden flex flex-col">
|
<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">
|
<div className="flex-1 overflow-auto min-h-0">
|
||||||
<TaskRegistry
|
<TaskRegistry
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
@@ -531,6 +510,8 @@ const TaskView = () => {
|
|||||||
onToggleSelectAll={selectAllVisible}
|
onToggleSelectAll={selectAllVisible}
|
||||||
onSelectRange={selectRange}
|
onSelectRange={selectRange}
|
||||||
onSetAnchorTaskId={setAnchorTaskId}
|
onSetAnchorTaskId={setAnchorTaskId}
|
||||||
|
onClearSelection={clearSelection}
|
||||||
|
onRequestBulkDelete={() => setDeleteDialogOpen(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user