Refactor dropdown menu components in various files to prevent event propagation, enhancing user interaction. Update CountedLineTabs component documentation with preview links for better clarity. Improve styling and layout in resource page and user columns for consistency and visual appeal.
This commit is contained in:
@@ -17,7 +17,11 @@ interface CountedLineTabsProps {
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
/** Line tabs with count pills (c-tabs-2 / data-grid-filtering-2). */
|
||||
/**
|
||||
* Line tabs with count pills — ReUI PRO DNA.
|
||||
* Preview: https://reui.io/preview/base/components/c-tabs-2
|
||||
* Lists: https://reui.io/preview/base/data-grid-filtering-2
|
||||
*/
|
||||
export function CountedLineTabs({
|
||||
tabs,
|
||||
value,
|
||||
@@ -28,12 +32,23 @@ export function CountedLineTabs({
|
||||
}: CountedLineTabsProps) {
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className={className}>
|
||||
<TabsList variant="line" className={cn('gap-5', listClassName)}>
|
||||
<TabsList
|
||||
variant="line"
|
||||
className={cn(
|
||||
'h-auto w-full justify-start gap-5 rounded-none bg-transparent p-0',
|
||||
listClassName,
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="text-muted-foreground hover:text-foreground h-auto gap-2 px-0 pb-3 after:bottom-0"
|
||||
className={cn(
|
||||
'text-muted-foreground hover:text-foreground h-auto flex-none gap-2 rounded-none px-0 pb-3 shadow-none',
|
||||
'data-active:bg-transparent data-active:text-foreground data-active:shadow-none',
|
||||
'dark:data-active:border-transparent dark:data-active:bg-transparent',
|
||||
'after:bottom-0 after:bg-foreground',
|
||||
)}
|
||||
>
|
||||
<span>{tab.label}</span>
|
||||
{tab.count !== undefined ? (
|
||||
|
||||
@@ -251,26 +251,32 @@ function RowActions({
|
||||
onOpen: (row: TelemtErrorRow) => void
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
aria-label="Действия"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon aria-hidden />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
||||
<EyeIcon className="size-4" aria-hidden />
|
||||
Открыть
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
aria-label="Действия"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon aria-hidden />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
||||
<EyeIcon className="size-4" aria-hidden />
|
||||
Открыть
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -570,14 +570,13 @@ function ResourcePageFiltered<T extends object>({
|
||||
<FramePanel className="p-0 shadow-none!">
|
||||
{countedTabs.length > 0 ? (
|
||||
<>
|
||||
<div className="px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
|
||||
<div className="border-border flex flex-col gap-0 border-b px-(--frame-panel-header-px) pt-(--frame-panel-header-py)">
|
||||
<CountedLineTabs
|
||||
tabs={countedTabs}
|
||||
value={activeTab}
|
||||
onValueChange={handleTabChange}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1269,7 +1269,21 @@ function DataGridTableBodyRow<TData>({
|
||||
data-depth={row.depth || undefined}
|
||||
data-row-pinned={isRowPinned || undefined}
|
||||
data-row-pinned-boundary={pinnedBoundary}
|
||||
onClick={() => props.onRowClick && props.onRowClick(row.original)}
|
||||
onClick={(event) => {
|
||||
if (!props.onRowClick) return
|
||||
// Interactive controls (actions menu, pin, checkbox, links) must not
|
||||
// open the row detail — same contract as DataGridTableRowPin/Select.
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof Element &&
|
||||
target.closest(
|
||||
'button, a, input, textarea, select, [role="button"], [role="menuitem"], [role="checkbox"], [data-slot="dropdown-menu-trigger"], [data-slot="checkbox"]'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
props.onRowClick(row.original)
|
||||
}}
|
||||
className={cn(
|
||||
"hover:bg-muted/40 data-[state=selected]:bg-muted/50",
|
||||
props.onRowClick && "cursor-pointer",
|
||||
|
||||
@@ -199,55 +199,61 @@ export function ActionsCell({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
aria-label="Действия строки"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start" className="w-44">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}>
|
||||
{isPinned ? (
|
||||
<PinOffIcon className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<Pin className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
{isPinned ? 'Открепить' : 'Закрепить'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
||||
<EyeIcon className="size-4" aria-hidden="true" />
|
||||
Открыть
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
copyToClipboard(row.original.username)
|
||||
toast.success('Имя скопировано', {
|
||||
description: row.original.username,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" aria-hidden="true" />
|
||||
Копировать имя
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2Icon className="size-4" aria-hidden="true" />
|
||||
Удалить
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div
|
||||
className="flex items-center justify-center"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="size-7"
|
||||
aria-label="Действия строки"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon aria-hidden="true" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start" className="w-44">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}>
|
||||
{isPinned ? (
|
||||
<PinOffIcon className="size-4" aria-hidden="true" />
|
||||
) : (
|
||||
<Pin className="size-4" aria-hidden="true" />
|
||||
)}
|
||||
{isPinned ? 'Открепить' : 'Закрепить'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
||||
<EyeIcon className="size-4" aria-hidden="true" />
|
||||
Открыть
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
copyToClipboard(row.original.username)
|
||||
toast.success('Имя скопировано', {
|
||||
description: row.original.username,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" aria-hidden="true" />
|
||||
Копировать имя
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
>
|
||||
<Trash2Icon className="size-4" aria-hidden="true" />
|
||||
Удалить
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<AlertDialogContent size="sm">
|
||||
|
||||
Reference in New Issue
Block a user