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
|
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({
|
export function CountedLineTabs({
|
||||||
tabs,
|
tabs,
|
||||||
value,
|
value,
|
||||||
@@ -28,12 +32,23 @@ export function CountedLineTabs({
|
|||||||
}: CountedLineTabsProps) {
|
}: CountedLineTabsProps) {
|
||||||
return (
|
return (
|
||||||
<Tabs value={value} onValueChange={onValueChange} className={className}>
|
<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) => (
|
{tabs.map((tab) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
value={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>
|
<span>{tab.label}</span>
|
||||||
{tab.count !== undefined ? (
|
{tab.count !== undefined ? (
|
||||||
|
|||||||
@@ -251,26 +251,32 @@ function RowActions({
|
|||||||
onOpen: (row: TelemtErrorRow) => void
|
onOpen: (row: TelemtErrorRow) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<div
|
||||||
<DropdownMenuTrigger
|
className="flex items-center justify-center"
|
||||||
render={
|
onClick={(event) => event.stopPropagation()}
|
||||||
<Button
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
size="icon"
|
>
|
||||||
variant="ghost"
|
<DropdownMenu>
|
||||||
className="size-7"
|
<DropdownMenuTrigger
|
||||||
aria-label="Действия"
|
render={
|
||||||
/>
|
<Button
|
||||||
}
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<MoreHorizontalIcon aria-hidden />
|
className="size-7"
|
||||||
</DropdownMenuTrigger>
|
aria-label="Действия"
|
||||||
<DropdownMenuContent align="end" className="w-40">
|
/>
|
||||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
}
|
||||||
<EyeIcon className="size-4" aria-hidden />
|
>
|
||||||
Открыть
|
<MoreHorizontalIcon aria-hidden />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuContent align="end" className="w-40">
|
||||||
</DropdownMenu>
|
<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!">
|
<FramePanel className="p-0 shadow-none!">
|
||||||
{countedTabs.length > 0 ? (
|
{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
|
<CountedLineTabs
|
||||||
tabs={countedTabs}
|
tabs={countedTabs}
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={handleTabChange}
|
onValueChange={handleTabChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -1269,7 +1269,21 @@ function DataGridTableBodyRow<TData>({
|
|||||||
data-depth={row.depth || undefined}
|
data-depth={row.depth || undefined}
|
||||||
data-row-pinned={isRowPinned || undefined}
|
data-row-pinned={isRowPinned || undefined}
|
||||||
data-row-pinned-boundary={pinnedBoundary}
|
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(
|
className={cn(
|
||||||
"hover:bg-muted/40 data-[state=selected]:bg-muted/50",
|
"hover:bg-muted/40 data-[state=selected]:bg-muted/50",
|
||||||
props.onRowClick && "cursor-pointer",
|
props.onRowClick && "cursor-pointer",
|
||||||
|
|||||||
@@ -199,55 +199,61 @@ export function ActionsCell({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<div
|
||||||
<DropdownMenuTrigger
|
className="flex items-center justify-center"
|
||||||
render={
|
onClick={(event) => event.stopPropagation()}
|
||||||
<Button
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
size="icon"
|
>
|
||||||
variant="ghost"
|
<DropdownMenu>
|
||||||
className="size-7"
|
<DropdownMenuTrigger
|
||||||
aria-label="Действия строки"
|
render={
|
||||||
/>
|
<Button
|
||||||
}
|
size="icon"
|
||||||
>
|
variant="ghost"
|
||||||
<MoreHorizontalIcon aria-hidden="true" />
|
className="size-7"
|
||||||
</DropdownMenuTrigger>
|
aria-label="Действия строки"
|
||||||
<DropdownMenuContent side="bottom" align="start" className="w-44">
|
/>
|
||||||
<DropdownMenuGroup>
|
}
|
||||||
<DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}>
|
>
|
||||||
{isPinned ? (
|
<MoreHorizontalIcon aria-hidden="true" />
|
||||||
<PinOffIcon className="size-4" aria-hidden="true" />
|
</DropdownMenuTrigger>
|
||||||
) : (
|
<DropdownMenuContent side="bottom" align="start" className="w-44">
|
||||||
<Pin className="size-4" aria-hidden="true" />
|
<DropdownMenuGroup>
|
||||||
)}
|
<DropdownMenuItem onClick={() => row.pin(isPinned ? false : 'top')}>
|
||||||
{isPinned ? 'Открепить' : 'Закрепить'}
|
{isPinned ? (
|
||||||
</DropdownMenuItem>
|
<PinOffIcon className="size-4" aria-hidden="true" />
|
||||||
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
) : (
|
||||||
<EyeIcon className="size-4" aria-hidden="true" />
|
<Pin className="size-4" aria-hidden="true" />
|
||||||
Открыть
|
)}
|
||||||
</DropdownMenuItem>
|
{isPinned ? 'Открепить' : 'Закрепить'}
|
||||||
<DropdownMenuItem
|
</DropdownMenuItem>
|
||||||
onClick={() => {
|
<DropdownMenuItem onClick={() => onOpen(row.original)}>
|
||||||
copyToClipboard(row.original.username)
|
<EyeIcon className="size-4" aria-hidden="true" />
|
||||||
toast.success('Имя скопировано', {
|
Открыть
|
||||||
description: row.original.username,
|
</DropdownMenuItem>
|
||||||
})
|
<DropdownMenuItem
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
copyToClipboard(row.original.username)
|
||||||
<CopyIcon className="size-4" aria-hidden="true" />
|
toast.success('Имя скопировано', {
|
||||||
Копировать имя
|
description: row.original.username,
|
||||||
</DropdownMenuItem>
|
})
|
||||||
<DropdownMenuSeparator />
|
}}
|
||||||
<DropdownMenuItem
|
>
|
||||||
variant="destructive"
|
<CopyIcon className="size-4" aria-hidden="true" />
|
||||||
onClick={() => setDeleteOpen(true)}
|
Копировать имя
|
||||||
>
|
</DropdownMenuItem>
|
||||||
<Trash2Icon className="size-4" aria-hidden="true" />
|
<DropdownMenuSeparator />
|
||||||
Удалить
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
variant="destructive"
|
||||||
</DropdownMenuGroup>
|
onClick={() => setDeleteOpen(true)}
|
||||||
</DropdownMenuContent>
|
>
|
||||||
</DropdownMenu>
|
<Trash2Icon className="size-4" aria-hidden="true" />
|
||||||
|
Удалить
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||||
<AlertDialogContent size="sm">
|
<AlertDialogContent size="sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user