feat: enhance internationalization with new translation keys, improve action handling in data tables, and add responsive view support for row actions

This commit is contained in:
Firman Ramdhani
2026-07-22 15:35:39 +07:00
parent 9a1422ef28
commit 0243b6aa96
10 changed files with 623 additions and 181 deletions
@@ -40,10 +40,12 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
}
const isPremiumGlow = action.intent === 'primary' && action.variant === 'filled';
const showLabel = action.showLabel !== false;
const tooltipContent = action.tooltipLabel || (!showLabel ? action.label : undefined);
// 1. Button with Dropdown (Menu.Target)
if (action.children && action.children.length > 0) {
const ButtonWithDropdown = (
const ButtonWithDropdown = showLabel ? (
<Button
variant={action.variant || 'transparent'}
color={action?.color ? action.color : getIntentColor(action.intent)}
@@ -55,14 +57,23 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
>
{action.label}
</Button>
) : (
<ActionIcon
variant={action.variant || 'transparent'}
color={action?.color ? action.color : getIntentColor(action.intent)}
disabled={action.disabled}
size="lg"
style={isPremiumGlow ? defaultButtonStyle(true).style : undefined}
>
{action.icon}
</ActionIcon>
);
return (
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
<Menu.Target>
{/* Shortcuts on the main button remain hidden in the Tooltip */}
{action.tooltipLabel ? (
<Tooltip position="bottom" label={`${action.tooltipLabel}`} withArrow openDelay={500}>
{tooltipContent ? (
<Tooltip position="bottom" label={tooltipContent} withArrow openDelay={500}>
{ButtonWithDropdown}
</Tooltip>
) : (
@@ -92,7 +103,7 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
}
// 2. Regular Button (Standalone)
const StandaloneButton = (
const StandaloneButton = showLabel ? (
<Button
variant={action.variant || 'transparent'}
color={action?.color ? action.color : getIntentColor(action.intent)}
@@ -104,10 +115,21 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
>
{action.label}
</Button>
) : (
<ActionIcon
variant={action.variant || 'transparent'}
color={action?.color ? action.color : getIntentColor(action.intent)}
disabled={action.disabled}
onClick={() => action.onClick?.(action.key || '')}
size="lg"
style={isPremiumGlow ? defaultButtonStyle(true).style : undefined}
>
{action.icon}
</ActionIcon>
);
return action.tooltipLabel ? (
<Tooltip position="bottom" key={action.key} label={`${action.tooltipLabel}`} withArrow openDelay={500}>
return tooltipContent ? (
<Tooltip position="bottom" key={action.key} label={tooltipContent} withArrow openDelay={500}>
{StandaloneButton}
</Tooltip>
) : (
@@ -8,6 +8,7 @@ export interface RowActionsProps {
/** Array of configured row-level actions. */
actions: RowActionProps[];
showLabels?: boolean;
responsiveView?: boolean;
}
/**
@@ -16,34 +17,54 @@ export interface RowActionsProps {
*
* @performance Wrapped in React.memo to guarantee zero overhead inside large lists/grids.
*/
export const RowActions = memo(function RowActions({ actions = [], showLabels = false }: RowActionsProps) {
export const RowActions = memo(function RowActions({
actions = [],
showLabels = false,
responsiveView = true,
}: RowActionsProps) {
/**
* Helper function to render a standalone icon button.
* Wraps the icon in a Tooltip if the configuration provides one.
* Helper function to render a standalone item.
*/
const renderIcon = (action: RowActionProps, fallbackKey: string) => {
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 = (
<Button
key={action.key}
variant={'transparent'}
<ActionIcon
key={actionKey}
variant="subtle"
color={getIntentColor(action.intent)}
leftSection={action.icon}
disabled={action.disabled}
onClick={() => action.onClick?.(action.key || '')}
size="xs"
pr="xs"
pl="xs"
pt={0}
pb={0}
onClick={handleClick}
size="md"
>
{showLabels && action.label}
</Button>
{action.icon}
</ActionIcon>
);
return action.tooltip ? (
<Tooltip key={`tooltip-${actionKey}`} label={action.tooltip} withArrow withinPortal>
<Tooltip key={`tooltip-${actionKey}`} label={action.tooltip} withArrow withinPortal zIndex={9999}>
{iconBtn}
</Tooltip>
) : (
@@ -51,104 +72,119 @@ export const RowActions = memo(function RowActions({ actions = [], showLabels =
);
};
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={0} wrap="nowrap" visibleFrom="sm">
{actions.map((action, index) => {
if (action.type === 'divider') {
return <Divider key={`divider-${index}`} orientation="vertical" mr="xs" ml="xs" />;
}
// Render Dropdown Menu for actions with children
if (action.children && action.children.length > 0) {
return (
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
<Menu.Target>{renderIcon(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={() => child.onClick?.(child.key || '')}
>
{child.label}
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
);
}
return renderIcon(action, `action-${index}`);
})}
<Group gap={0} wrap="nowrap" visibleFrom={responsiveView ? 'sm' : undefined}>
{renderFlatActions()}
</Group>
{/* --- MOBILE VIEW (hidden on desktop devices) --- */}
<Group gap={0} wrap="nowrap" hiddenFrom="sm">
<Menu position="bottom-end" withArrow withinPortal>
<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) {
{responsiveView && (
<Group gap={0} 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 (
<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={() => child.onClick?.(child.key || '')}
style={{ paddingLeft: '1.5rem' }} // Indent nested items
mt="sm"
mb="sm"
>
{child.label}
</Menu.Item>
);
})}
</Fragment>
<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>
);
}
return (
<Menu.Item
key={action.key}
leftSection={action.icon}
color={getIntentColor(action.intent)}
disabled={action.disabled}
onClick={() => action.onClick?.(action.key || '')}
mt="sm"
mb="sm"
>
{action.label}
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
</Group>
})}
</Menu.Dropdown>
</Menu>
</Group>
)}
</Box>
);
});
@@ -44,6 +44,8 @@ export interface PageActionProps extends BaseAction {
/** Human-readable keyboard tooltip label (e.g., '⇧⌘N'). Shown in tooltip. */
tooltipLabel?: string;
/** Whether to show the text label for the main action button. Defaults to true. If false, renders as ActionIcon. */
showLabel?: boolean;
}
/**
@@ -207,7 +207,7 @@ export const DEFAULT_STATUS_MAP: Record<string, BadgeProps> = {
[STATUS_DATA.PRESENT]: { color: '#E2B43E', leftSection: getIcon(Users) },
};
export function StatusBadge({ status, label, getCustomConfig, color, leftSection, ...rest }: StatusBadgeProps) {
export function StatusBadge({ status, label, getCustomConfig, color, ...rest }: StatusBadgeProps) {
if (!status) return null;
const normalizedStatus = status?.toLowerCase() || '';
@@ -222,7 +222,6 @@ export function StatusBadge({ status, label, getCustomConfig, color, leftSection
style={{ textTransform: 'capitalize' }}
variant={customConfig.variant ? customConfig.variant : 'light'}
color={color || customConfig.color || defaultConfig.color}
leftSection={leftSection || customConfig.leftSection || defaultConfig.leftSection}
{...rest}
>
{label || (status ? status.replace(/-/g, ' ') : 'Unknown')}