191 lines
6.3 KiB
TypeScript
191 lines
6.3 KiB
TypeScript
import { Fragment, memo } from 'react';
|
|
import { Group, ActionIcon, Tooltip, Menu, Divider, Box, Button, MantineSpacing } from '@mantine/core';
|
|
import { RowActionProps } from './types';
|
|
import { getIntentColor } from './utils';
|
|
import { MoreVertical } from 'lucide-react';
|
|
|
|
export interface RowActionsProps {
|
|
/** Array of configured row-level actions. */
|
|
actions: RowActionProps[];
|
|
showLabels?: boolean;
|
|
responsiveView?: boolean;
|
|
gapActionDesktop?: MantineSpacing;
|
|
gapActionMobile?: MantineSpacing;
|
|
}
|
|
|
|
/**
|
|
* A lightweight presentational component optimized for rendering inside data grid rows.
|
|
* Automatically handles tooltip generation and constructs dropdown menus for nested actions.
|
|
*
|
|
* @performance Wrapped in React.memo to guarantee zero overhead inside large lists/grids.
|
|
*/
|
|
export const RowActions = memo(function RowActions(props: RowActionsProps) {
|
|
const { actions = [], showLabels = false, responsiveView = true, gapActionDesktop = 0, gapActionMobile = 0 } = props;
|
|
|
|
/**
|
|
* Helper function to render a standalone item.
|
|
*/
|
|
const renderItem = (action: RowActionProps, fallbackKey: string) => {
|
|
const actionKey = action.key || fallbackKey;
|
|
const isButton = showLabels;
|
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
action.onClick?.(action.key || '');
|
|
};
|
|
|
|
if (isButton) {
|
|
return (
|
|
<Button
|
|
key={actionKey}
|
|
variant="subtle"
|
|
color={getIntentColor(action.intent)}
|
|
leftSection={action.icon}
|
|
disabled={action.disabled}
|
|
onClick={handleClick}
|
|
size="xs"
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
const iconBtn = (
|
|
<ActionIcon
|
|
key={actionKey}
|
|
variant="subtle"
|
|
color={getIntentColor(action.intent)}
|
|
disabled={action.disabled}
|
|
onClick={handleClick}
|
|
size="md"
|
|
>
|
|
{action.icon}
|
|
</ActionIcon>
|
|
);
|
|
|
|
return action.tooltip ? (
|
|
<Tooltip key={`tooltip-${actionKey}`} label={action.tooltip} withArrow withinPortal zIndex={9999}>
|
|
{iconBtn}
|
|
</Tooltip>
|
|
) : (
|
|
iconBtn
|
|
);
|
|
};
|
|
|
|
const renderFlatActions = () => {
|
|
return actions.map((action, index) => {
|
|
if (action.type === 'divider') {
|
|
return <Divider key={`divider-${index}`} orientation="vertical" mr="xs" ml="xs" />;
|
|
}
|
|
|
|
if (action.children && action.children.length > 0) {
|
|
return (
|
|
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover" zIndex={9999}>
|
|
<Menu.Target>{renderItem(action, `action-${index}`)}</Menu.Target>
|
|
<Menu.Dropdown>
|
|
{action.children.map((child, childIndex) => {
|
|
if (child.type === 'divider') {
|
|
return <Menu.Divider key={`child-divider-${childIndex}`} />;
|
|
}
|
|
return (
|
|
<Menu.Item
|
|
key={child.key}
|
|
leftSection={child.icon}
|
|
color={getIntentColor(child.intent)}
|
|
disabled={child.disabled}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
child.onClick?.(child.key || '');
|
|
}}
|
|
>
|
|
{child.label}
|
|
</Menu.Item>
|
|
);
|
|
})}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
);
|
|
}
|
|
|
|
return renderItem(action, `action-${index}`);
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Box style={{ display: 'inline-flex' }}>
|
|
{/* --- DESKTOP VIEW (hidden on mobile devices) --- */}
|
|
<Group gap={gapActionDesktop} wrap="nowrap" visibleFrom={responsiveView ? 'sm' : undefined}>
|
|
{renderFlatActions()}
|
|
</Group>
|
|
|
|
{/* --- MOBILE VIEW (hidden on desktop devices) --- */}
|
|
|
|
{responsiveView && (
|
|
<Group gap={gapActionMobile} wrap="nowrap" hiddenFrom="sm">
|
|
<Menu position="bottom-end" withArrow withinPortal zIndex={9999}>
|
|
<Menu.Target>
|
|
<ActionIcon variant="transparent" size="md">
|
|
<MoreVertical size={16} />
|
|
</ActionIcon>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
{actions.map((action, index) => {
|
|
if (action.type === 'divider') {
|
|
return <Menu.Divider key={`mobile-divider-${index}`} mt="sm" mb="sm" />;
|
|
}
|
|
|
|
if (action.children && action.children.length > 0) {
|
|
return (
|
|
<Fragment key={action.key}>
|
|
<Menu.Label>{action.label}</Menu.Label>
|
|
{action.children.map((child, childIndex) => {
|
|
if (child.type === 'divider') {
|
|
return <Menu.Divider key={`mobile-child-divider-${childIndex}`} mt="xs" mb="xs" />;
|
|
}
|
|
return (
|
|
<Menu.Item
|
|
key={child.key}
|
|
leftSection={child.icon}
|
|
color={getIntentColor(child.intent)}
|
|
disabled={child.disabled}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
child.onClick?.(child.key || '');
|
|
}}
|
|
style={{ paddingLeft: '1.5rem' }}
|
|
mt="sm"
|
|
mb="sm"
|
|
>
|
|
{child.label}
|
|
</Menu.Item>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Menu.Item
|
|
key={action.key}
|
|
leftSection={action.icon}
|
|
color={getIntentColor(action.intent)}
|
|
disabled={action.disabled}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
action.onClick?.(action.key || '');
|
|
}}
|
|
mt="sm"
|
|
mb="sm"
|
|
>
|
|
{action.label}
|
|
</Menu.Item>
|
|
);
|
|
})}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
</Group>
|
|
)}
|
|
</Box>
|
|
);
|
|
});
|