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:
@@ -58,7 +58,9 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"setting": "Setting"
|
"setting": "Setting",
|
||||||
|
"detail": "Detail",
|
||||||
|
"view": "View"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"delete": {
|
"delete": {
|
||||||
|
|||||||
@@ -58,7 +58,9 @@
|
|||||||
"back": "Kembali",
|
"back": "Kembali",
|
||||||
"reload": "Muat Ulang",
|
"reload": "Muat Ulang",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"setting": "Pengaturan"
|
"setting": "Pengaturan",
|
||||||
|
"detail": "Detail",
|
||||||
|
"view": "Lihat"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"delete": {
|
"delete": {
|
||||||
|
|||||||
@@ -40,10 +40,12 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isPremiumGlow = action.intent === 'primary' && action.variant === 'filled';
|
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)
|
// 1. Button with Dropdown (Menu.Target)
|
||||||
if (action.children && action.children.length > 0) {
|
if (action.children && action.children.length > 0) {
|
||||||
const ButtonWithDropdown = (
|
const ButtonWithDropdown = showLabel ? (
|
||||||
<Button
|
<Button
|
||||||
variant={action.variant || 'transparent'}
|
variant={action.variant || 'transparent'}
|
||||||
color={action?.color ? action.color : getIntentColor(action.intent)}
|
color={action?.color ? action.color : getIntentColor(action.intent)}
|
||||||
@@ -55,14 +57,23 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Button>
|
</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 (
|
return (
|
||||||
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
{/* Shortcuts on the main button remain hidden in the Tooltip */}
|
{tooltipContent ? (
|
||||||
{action.tooltipLabel ? (
|
<Tooltip position="bottom" label={tooltipContent} withArrow openDelay={500}>
|
||||||
<Tooltip position="bottom" label={`${action.tooltipLabel}`} withArrow openDelay={500}>
|
|
||||||
{ButtonWithDropdown}
|
{ButtonWithDropdown}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -92,7 +103,7 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Regular Button (Standalone)
|
// 2. Regular Button (Standalone)
|
||||||
const StandaloneButton = (
|
const StandaloneButton = showLabel ? (
|
||||||
<Button
|
<Button
|
||||||
variant={action.variant || 'transparent'}
|
variant={action.variant || 'transparent'}
|
||||||
color={action?.color ? action.color : getIntentColor(action.intent)}
|
color={action?.color ? action.color : getIntentColor(action.intent)}
|
||||||
@@ -104,10 +115,21 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Button>
|
</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 ? (
|
return tooltipContent ? (
|
||||||
<Tooltip position="bottom" key={action.key} label={`${action.tooltipLabel}`} withArrow openDelay={500}>
|
<Tooltip position="bottom" key={action.key} label={tooltipContent} withArrow openDelay={500}>
|
||||||
{StandaloneButton}
|
{StandaloneButton}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface RowActionsProps {
|
|||||||
/** Array of configured row-level actions. */
|
/** Array of configured row-level actions. */
|
||||||
actions: RowActionProps[];
|
actions: RowActionProps[];
|
||||||
showLabels?: boolean;
|
showLabels?: boolean;
|
||||||
|
responsiveView?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,34 +17,54 @@ export interface RowActionsProps {
|
|||||||
*
|
*
|
||||||
* @performance Wrapped in React.memo to guarantee zero overhead inside large lists/grids.
|
* @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.
|
* Helper function to render a standalone item.
|
||||||
* Wraps the icon in a Tooltip if the configuration provides one.
|
|
||||||
*/
|
*/
|
||||||
const renderIcon = (action: RowActionProps, fallbackKey: string) => {
|
const renderItem = (action: RowActionProps, fallbackKey: string) => {
|
||||||
const actionKey = action.key || fallbackKey;
|
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 = (
|
const iconBtn = (
|
||||||
<Button
|
<ActionIcon
|
||||||
key={action.key}
|
key={actionKey}
|
||||||
variant={'transparent'}
|
variant="subtle"
|
||||||
color={getIntentColor(action.intent)}
|
color={getIntentColor(action.intent)}
|
||||||
leftSection={action.icon}
|
|
||||||
disabled={action.disabled}
|
disabled={action.disabled}
|
||||||
onClick={() => action.onClick?.(action.key || '')}
|
onClick={handleClick}
|
||||||
size="xs"
|
size="md"
|
||||||
pr="xs"
|
|
||||||
pl="xs"
|
|
||||||
pt={0}
|
|
||||||
pb={0}
|
|
||||||
>
|
>
|
||||||
{showLabels && action.label}
|
{action.icon}
|
||||||
</Button>
|
</ActionIcon>
|
||||||
);
|
);
|
||||||
|
|
||||||
return action.tooltip ? (
|
return action.tooltip ? (
|
||||||
<Tooltip key={`tooltip-${actionKey}`} label={action.tooltip} withArrow withinPortal>
|
<Tooltip key={`tooltip-${actionKey}`} label={action.tooltip} withArrow withinPortal zIndex={9999}>
|
||||||
{iconBtn}
|
{iconBtn}
|
||||||
</Tooltip>
|
</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 (
|
return (
|
||||||
<Box style={{ display: 'inline-flex' }}>
|
<Box style={{ display: 'inline-flex' }}>
|
||||||
{/* --- DESKTOP VIEW (hidden on mobile devices) --- */}
|
{/* --- DESKTOP VIEW (hidden on mobile devices) --- */}
|
||||||
<Group gap={0} wrap="nowrap" visibleFrom="sm">
|
<Group gap={0} wrap="nowrap" visibleFrom={responsiveView ? 'sm' : undefined}>
|
||||||
{actions.map((action, index) => {
|
{renderFlatActions()}
|
||||||
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>
|
</Group>
|
||||||
|
|
||||||
{/* --- MOBILE VIEW (hidden on desktop devices) --- */}
|
{/* --- 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 (
|
return (
|
||||||
<Fragment key={action.key}>
|
<Menu.Item
|
||||||
<Menu.Label>{action.label}</Menu.Label>
|
key={action.key}
|
||||||
{action.children.map((child, childIndex) => {
|
leftSection={action.icon}
|
||||||
if (child.type === 'divider') {
|
color={getIntentColor(action.intent)}
|
||||||
return <Menu.Divider key={`mobile-child-divider-${childIndex}`} mt="xs" mb="xs" />;
|
disabled={action.disabled}
|
||||||
}
|
onClick={(e) => {
|
||||||
return (
|
e.stopPropagation();
|
||||||
<Menu.Item
|
action.onClick?.(action.key || '');
|
||||||
key={child.key}
|
}}
|
||||||
leftSection={child.icon}
|
mt="sm"
|
||||||
color={getIntentColor(child.intent)}
|
mb="sm"
|
||||||
disabled={child.disabled}
|
>
|
||||||
onClick={() => child.onClick?.(child.key || '')}
|
{action.label}
|
||||||
style={{ paddingLeft: '1.5rem' }} // Indent nested items
|
</Menu.Item>
|
||||||
mt="sm"
|
|
||||||
mb="sm"
|
|
||||||
>
|
|
||||||
{child.label}
|
|
||||||
</Menu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
|
</Menu.Dropdown>
|
||||||
return (
|
</Menu>
|
||||||
<Menu.Item
|
</Group>
|
||||||
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>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export interface PageActionProps extends BaseAction {
|
|||||||
|
|
||||||
/** Human-readable keyboard tooltip label (e.g., '⇧⌘N'). Shown in tooltip. */
|
/** Human-readable keyboard tooltip label (e.g., '⇧⌘N'). Shown in tooltip. */
|
||||||
tooltipLabel?: string;
|
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) },
|
[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;
|
if (!status) return null;
|
||||||
|
|
||||||
const normalizedStatus = status?.toLowerCase() || '';
|
const normalizedStatus = status?.toLowerCase() || '';
|
||||||
@@ -222,7 +222,6 @@ export function StatusBadge({ status, label, getCustomConfig, color, leftSection
|
|||||||
style={{ textTransform: 'capitalize' }}
|
style={{ textTransform: 'capitalize' }}
|
||||||
variant={customConfig.variant ? customConfig.variant : 'light'}
|
variant={customConfig.variant ? customConfig.variant : 'light'}
|
||||||
color={color || customConfig.color || defaultConfig.color}
|
color={color || customConfig.color || defaultConfig.color}
|
||||||
leftSection={leftSection || customConfig.leftSection || defaultConfig.leftSection}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{label || (status ? status.replace(/-/g, ' ') : 'Unknown')}
|
{label || (status ? status.replace(/-/g, ' ') : 'Unknown')}
|
||||||
|
|||||||
+120
-46
@@ -1,77 +1,151 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Menu, ActionIcon } from '@mantine/core';
|
import { Eye, Trash2, CheckCircle, XCircle, Edit2, Copy, PauseCircle, RotateCcw, X, Check } from 'lucide-react';
|
||||||
import { MoreVertical, Eye, Trash2, CheckCircle, XCircle } from 'lucide-react';
|
|
||||||
import { ModuleAction, ModuleActionType } from '../../../entities/entity';
|
import { ModuleAction, ModuleActionType } from '../../../entities/entity';
|
||||||
import { useEnterpriseModuleTranslationContext } from '../../../hooks/use-module.context';
|
import {
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
|
} from '../../../hooks/use-module.context';
|
||||||
|
import { RowActionProps, RowActions } from '../../../../../components';
|
||||||
|
|
||||||
export interface RowActionMenuProps {
|
export interface RowActionMenuProps {
|
||||||
data: any;
|
data: any;
|
||||||
rowIndex: number;
|
onActionClick: (action: ModuleActionType | 'VIEW', data: any) => void;
|
||||||
onActionClick: (action: ModuleActionType, data: any) => void;
|
|
||||||
statusKey?: string;
|
statusKey?: string;
|
||||||
customActions?: (data: any, defaultActions: any[]) => any[];
|
customActions?: (data: any, defaultActions: RowActionProps[]) => RowActionProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RowActionMenu({ data, onActionClick, statusKey = 'status', customActions }: RowActionMenuProps) {
|
export function RowActionMenu({ data, onActionClick, statusKey = 'status', customActions }: RowActionMenuProps) {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const { privileges, config } = useEnterpriseModuleConfigContext();
|
||||||
|
const { moduleType } = config;
|
||||||
|
|
||||||
const status = data?.[statusKey]?.toLowerCase();
|
const status = data?.[statusKey]?.toLowerCase();
|
||||||
|
|
||||||
const defaultActions = useMemo(() => {
|
const defaultActions = useMemo<RowActionProps[]>(() => {
|
||||||
const actions: any[] = [];
|
const actions: RowActionProps[] = [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
ALLOW_EDIT,
|
||||||
|
ALLOW_DELETE,
|
||||||
|
ALLOW_CREATE,
|
||||||
|
ALLOW_ACTIVATE,
|
||||||
|
ALLOW_DEACTIVATE,
|
||||||
|
ALLOW_CONFIRM,
|
||||||
|
ALLOW_CANCEL,
|
||||||
|
ALLOW_ROLLBACK,
|
||||||
|
ALLOW_HOLD,
|
||||||
|
} = privileges;
|
||||||
|
|
||||||
|
const isTransaction = moduleType === 'TRANSACTION';
|
||||||
|
const isMasterData = moduleType === 'MASTER_DATA';
|
||||||
|
|
||||||
|
const isDataActive = status === 'active';
|
||||||
|
const isDataInActive = status === 'inactive' || status === 'draft';
|
||||||
|
|
||||||
// View Details
|
// View Details
|
||||||
actions.push({
|
actions.push({
|
||||||
key: 'VIEW',
|
key: 'VIEW',
|
||||||
label: t('common:actions.detail'),
|
label: t('common:actions.detail'),
|
||||||
|
tooltip: t('common:actions.detail'),
|
||||||
icon: <Eye size={14} />,
|
icon: <Eye size={14} />,
|
||||||
onClick: () => onActionClick('VIEW' as any, data),
|
onClick: () => onActionClick('VIEW' as any, data),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Active/Inactive toggle
|
if (ALLOW_EDIT) {
|
||||||
if (status === 'active') {
|
|
||||||
actions.push({
|
actions.push({
|
||||||
key: ModuleAction.DEACTIVATE,
|
key: ModuleAction.EDIT,
|
||||||
label: t('common:actions.deactivate'),
|
label: t('common:actions.edit'),
|
||||||
icon: <XCircle size={14} />,
|
tooltip: t('common:actions.edit'),
|
||||||
onClick: () => onActionClick(ModuleAction.DEACTIVATE, data),
|
icon: <Edit2 size={14} />,
|
||||||
});
|
onClick: () => onActionClick(ModuleAction.EDIT, data),
|
||||||
} else if (status === 'inactive') {
|
|
||||||
actions.push({
|
|
||||||
key: ModuleAction.ACTIVATE,
|
|
||||||
label: t('common:actions.activate'),
|
|
||||||
icon: <CheckCircle size={14} />,
|
|
||||||
onClick: () => onActionClick(ModuleAction.ACTIVATE, data),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete
|
if (ALLOW_CREATE) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: ModuleAction.DELETE,
|
key: ModuleAction.DUPLICATE,
|
||||||
label: t('common:actions.delete'),
|
label: t('common:actions.duplicate'),
|
||||||
icon: <Trash2 size={14} />,
|
tooltip: t('common:actions.duplicate'),
|
||||||
color: 'red',
|
icon: <Copy size={14} />,
|
||||||
onClick: () => onActionClick(ModuleAction.DELETE, data),
|
onClick: () => onActionClick(ModuleAction.DUPLICATE, data),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMasterData) {
|
||||||
|
if (ALLOW_DEACTIVATE && isDataActive) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.DEACTIVATE,
|
||||||
|
label: t('common:actions.deactivate'),
|
||||||
|
tooltip: t('common:actions.deactivate'),
|
||||||
|
icon: <XCircle size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.DEACTIVATE, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ALLOW_ACTIVATE && isDataInActive) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.ACTIVATE,
|
||||||
|
label: t('common:actions.activate'),
|
||||||
|
tooltip: t('common:actions.activate'),
|
||||||
|
icon: <CheckCircle size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.ACTIVATE, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTransaction) {
|
||||||
|
if (ALLOW_HOLD) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.HOLD,
|
||||||
|
label: t('common:actions.hold'),
|
||||||
|
tooltip: t('common:actions.hold'),
|
||||||
|
icon: <PauseCircle size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.HOLD, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ALLOW_ROLLBACK) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.ROLLBACK,
|
||||||
|
label: t('common:actions.rollback'),
|
||||||
|
tooltip: t('common:actions.rollback'),
|
||||||
|
icon: <RotateCcw size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.ROLLBACK, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ALLOW_CANCEL) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.CANCEL,
|
||||||
|
label: t('common:actions.cancel'),
|
||||||
|
tooltip: t('common:actions.cancel'),
|
||||||
|
icon: <X size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.CANCEL, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (ALLOW_CONFIRM) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.CONFIRM,
|
||||||
|
label: t('common:actions.confirm'),
|
||||||
|
tooltip: t('common:actions.confirm'),
|
||||||
|
icon: <Check size={14} />,
|
||||||
|
onClick: () => onActionClick(ModuleAction.CONFIRM, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ALLOW_DELETE) {
|
||||||
|
actions.push({
|
||||||
|
key: ModuleAction.DELETE,
|
||||||
|
label: t('common:actions.delete'),
|
||||||
|
tooltip: t('common:actions.delete'),
|
||||||
|
icon: <Trash2 size={14} />,
|
||||||
|
intent: 'destructive',
|
||||||
|
onClick: () => onActionClick(ModuleAction.DELETE, data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}, [status, t, onActionClick, data]);
|
}, [status, t, onActionClick, data, privileges, moduleType]);
|
||||||
|
|
||||||
const finalActions = customActions ? customActions(data, defaultActions) : defaultActions;
|
const finalActions = customActions ? customActions(data, defaultActions) : defaultActions;
|
||||||
|
|
||||||
return (
|
return <RowActions actions={finalActions} responsiveView={false} />;
|
||||||
<Menu position="bottom-end" withinPortal>
|
|
||||||
<Menu.Target>
|
|
||||||
<ActionIcon variant="subtle" color="gray">
|
|
||||||
<MoreVertical size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
{finalActions.map((action, idx) => (
|
|
||||||
<Menu.Item key={action.key || idx} color={action.color} leftSection={action.icon} onClick={action.onClick}>
|
|
||||||
{action.label}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useCallback, useRef } from 'react';
|
import { useMemo, useCallback, useRef, useState } from 'react';
|
||||||
import { AgGridReactProps } from 'ag-grid-react';
|
import { AgGridReactProps } from 'ag-grid-react';
|
||||||
import {
|
import {
|
||||||
ColDef,
|
ColDef,
|
||||||
@@ -16,12 +16,17 @@ import { Box } from '@mantine/core';
|
|||||||
import { DataGrid, StatusBadge } from '../../../../components';
|
import { DataGrid, StatusBadge } from '../../../../components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
useEnterpriseModuleDataServiceContext,
|
useEnterpriseModuleDataServiceContext,
|
||||||
useEnterpriseModuleSelectionContext,
|
useEnterpriseModuleSelectionContext,
|
||||||
useEnterpriseModuleTranslationContext,
|
useEnterpriseModuleTranslationContext,
|
||||||
|
useEnterpriseModuleNavigationContext,
|
||||||
} from '../../hooks/use-module.context';
|
} from '../../hooks/use-module.context';
|
||||||
import { BaseEntity } from '@repo/core-api/data-services';
|
import { BaseEntity } from '@repo/core-api/data-services';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { ModuleActionType, ModuleAction, ActionModalState, ActionModalConfig } from '../../entities/entity';
|
||||||
|
import { RowActionMenu } from './components/row-actions';
|
||||||
|
import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../action-confirmation-modal';
|
||||||
|
|
||||||
export * from 'ag-grid-community';
|
export * from 'ag-grid-community';
|
||||||
export * from 'ag-grid-react';
|
export * from 'ag-grid-react';
|
||||||
@@ -60,6 +65,30 @@ export interface EnterpriseDataTableProps<E extends BaseEntity> extends Omit<AgG
|
|||||||
noRowsMessage?: string;
|
noRowsMessage?: string;
|
||||||
|
|
||||||
showStatusbar?: boolean;
|
showStatusbar?: boolean;
|
||||||
|
|
||||||
|
customPrefixColumn?: (col: ColDef<E>[]) => ColDef<E>[];
|
||||||
|
|
||||||
|
// Action related props
|
||||||
|
statusKey?: string;
|
||||||
|
customRowActions?: (data: E, defaultActions: any[]) => any[];
|
||||||
|
onClickView?: (data: E) => void;
|
||||||
|
onClickEdit?: (data: E) => void;
|
||||||
|
onClickDuplicate?: (data: E) => void;
|
||||||
|
onClickDelete?: (data: E) => void;
|
||||||
|
onClickActivate?: (data: E) => void;
|
||||||
|
onClickDeactivate?: (data: E) => void;
|
||||||
|
onClickConfirm?: (data: E) => void;
|
||||||
|
onClickCancel?: (data: E) => void;
|
||||||
|
onClickRollback?: (data: E) => void;
|
||||||
|
onClickHold?: (data: E) => void;
|
||||||
|
|
||||||
|
deleteModalConfig?: ActionModalConfig;
|
||||||
|
activateModalConfig?: ActionModalConfig;
|
||||||
|
deactivateModalConfig?: ActionModalConfig;
|
||||||
|
confirmModalConfig?: ActionModalConfig;
|
||||||
|
cancelModalConfig?: ActionModalConfig;
|
||||||
|
rollbackModalConfig?: ActionModalConfig;
|
||||||
|
holdModalConfig?: ActionModalConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -77,6 +106,29 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
loadingMessage = 'Loading data...',
|
loadingMessage = 'Loading data...',
|
||||||
noRowsMessage = 'No records found',
|
noRowsMessage = 'No records found',
|
||||||
showStatusbar,
|
showStatusbar,
|
||||||
|
customPrefixColumn,
|
||||||
|
|
||||||
|
// new action props
|
||||||
|
statusKey = 'status',
|
||||||
|
customRowActions,
|
||||||
|
onClickView,
|
||||||
|
onClickEdit,
|
||||||
|
onClickDuplicate,
|
||||||
|
onClickDelete,
|
||||||
|
onClickActivate,
|
||||||
|
onClickDeactivate,
|
||||||
|
onClickConfirm,
|
||||||
|
onClickCancel,
|
||||||
|
onClickRollback,
|
||||||
|
onClickHold,
|
||||||
|
deleteModalConfig,
|
||||||
|
activateModalConfig,
|
||||||
|
deactivateModalConfig,
|
||||||
|
confirmModalConfig,
|
||||||
|
cancelModalConfig,
|
||||||
|
rollbackModalConfig,
|
||||||
|
holdModalConfig,
|
||||||
|
|
||||||
...restAgGridProps
|
...restAgGridProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -86,6 +138,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
||||||
const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
|
const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
|
||||||
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
|
||||||
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
|
const { moduleType } = config;
|
||||||
|
const isTransaction = moduleType === 'TRANSACTION';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Local UI State
|
// Local UI State
|
||||||
@@ -94,12 +151,186 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
// Reference to the AG Grid API for programmatic interaction
|
// Reference to the AG Grid API for programmatic interaction
|
||||||
const gridApiRef = useRef<GridApi<E> | null>(null);
|
const gridApiRef = useRef<GridApi<E> | null>(null);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Action Handlers & Modal State
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const CLOSED_MODAL: ActionModalState<E> = useMemo(() => ({ opened: false, action: null, data: null }), []);
|
||||||
|
const [actionModalState, setActionModalState] = useState<ActionModalState<E>>(CLOSED_MODAL);
|
||||||
|
|
||||||
|
const modalConfigMap = useMemo(
|
||||||
|
() => ({
|
||||||
|
[ModuleAction.DELETE]: deleteModalConfig,
|
||||||
|
[ModuleAction.ACTIVATE]: activateModalConfig,
|
||||||
|
[ModuleAction.DEACTIVATE]: deactivateModalConfig,
|
||||||
|
[ModuleAction.CONFIRM]: confirmModalConfig,
|
||||||
|
[ModuleAction.CANCEL]: cancelModalConfig,
|
||||||
|
[ModuleAction.ROLLBACK]: rollbackModalConfig,
|
||||||
|
[ModuleAction.HOLD]: holdModalConfig,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
deleteModalConfig,
|
||||||
|
activateModalConfig,
|
||||||
|
deactivateModalConfig,
|
||||||
|
confirmModalConfig,
|
||||||
|
cancelModalConfig,
|
||||||
|
rollbackModalConfig,
|
||||||
|
holdModalConfig,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const openActionModal = useCallback(
|
||||||
|
(action: ModuleActionType, data: E) => {
|
||||||
|
const config = modalConfigMap[action as keyof typeof modalConfigMap];
|
||||||
|
setActionModalState({ opened: true, action, data, config });
|
||||||
|
},
|
||||||
|
[modalConfigMap],
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeActionModal = useCallback(() => {
|
||||||
|
setActionModalState(CLOSED_MODAL);
|
||||||
|
}, [CLOSED_MODAL]);
|
||||||
|
|
||||||
|
const executeAction = useCallback(
|
||||||
|
async (action: ModuleActionType, data: E, meta?: Record<string, unknown>) => {
|
||||||
|
const id = data.id;
|
||||||
|
if (!id) return;
|
||||||
|
const currentConfig = modalConfigMap[action as keyof typeof modalConfigMap];
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case ModuleAction.DELETE:
|
||||||
|
await dataServices.delete(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.ACTIVATE:
|
||||||
|
await dataServices.activate(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.DEACTIVATE:
|
||||||
|
await dataServices.deactivate(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.CONFIRM:
|
||||||
|
await dataServices.confirmData(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.CANCEL:
|
||||||
|
await dataServices.cancelData(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.ROLLBACK:
|
||||||
|
await dataServices.rollbackData(id, meta);
|
||||||
|
break;
|
||||||
|
case ModuleAction.HOLD:
|
||||||
|
await dataServices.holdData(id, meta);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[executeAction] Unhandled action: ${action}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
|
||||||
|
const defaultSuccessMessage = t('common:notifications.actionSuccess', { action: t(actionKey) });
|
||||||
|
const customSuccessMessage =
|
||||||
|
typeof currentConfig?.successMessage === 'function'
|
||||||
|
? currentConfig.successMessage(data)
|
||||||
|
: currentConfig?.successMessage;
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: t('common:notifications.successTitle'),
|
||||||
|
message: customSuccessMessage || defaultSuccessMessage,
|
||||||
|
color: 'teal',
|
||||||
|
});
|
||||||
|
|
||||||
|
closeActionModal();
|
||||||
|
if (gridApiRef.current) {
|
||||||
|
gridApiRef.current.refreshServerSide({ purge: false });
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
|
||||||
|
const defaultErrorMessage = t('common:notifications.actionFailed', {
|
||||||
|
action: t(actionKey),
|
||||||
|
message: error?.message || 'Unknown error',
|
||||||
|
});
|
||||||
|
const customErrorMessage =
|
||||||
|
typeof currentConfig?.errorMessage === 'function'
|
||||||
|
? currentConfig.errorMessage(error)
|
||||||
|
: currentConfig?.errorMessage;
|
||||||
|
|
||||||
|
notifications.show({
|
||||||
|
title: t('common:notifications.errorTitle'),
|
||||||
|
message: customErrorMessage || defaultErrorMessage,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dataServices, closeActionModal, modalConfigMap, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleActionClick = useCallback(
|
||||||
|
(action: ModuleActionType | 'VIEW', data: E) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'VIEW':
|
||||||
|
if (onClickView) onClickView(data);
|
||||||
|
else navigation.navigateToDetail(data.id as string);
|
||||||
|
break;
|
||||||
|
case ModuleAction.EDIT:
|
||||||
|
if (onClickEdit) onClickEdit(data);
|
||||||
|
else navigation.navigateToEdit(data.id as string);
|
||||||
|
break;
|
||||||
|
case ModuleAction.DUPLICATE:
|
||||||
|
if (onClickDuplicate) onClickDuplicate(data);
|
||||||
|
else navigation.navigateToDuplicate(data.id as string);
|
||||||
|
break;
|
||||||
|
case ModuleAction.DELETE:
|
||||||
|
if (onClickDelete) onClickDelete(data);
|
||||||
|
else openActionModal(ModuleAction.DELETE, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.ACTIVATE:
|
||||||
|
if (onClickActivate) onClickActivate(data);
|
||||||
|
else openActionModal(ModuleAction.ACTIVATE, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.DEACTIVATE:
|
||||||
|
if (onClickDeactivate) onClickDeactivate(data);
|
||||||
|
else openActionModal(ModuleAction.DEACTIVATE, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.CONFIRM:
|
||||||
|
if (onClickConfirm) onClickConfirm(data);
|
||||||
|
else openActionModal(ModuleAction.CONFIRM, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.CANCEL:
|
||||||
|
if (onClickCancel) onClickCancel(data);
|
||||||
|
else openActionModal(ModuleAction.CANCEL, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.ROLLBACK:
|
||||||
|
if (onClickRollback) onClickRollback(data);
|
||||||
|
else openActionModal(ModuleAction.ROLLBACK, data);
|
||||||
|
break;
|
||||||
|
case ModuleAction.HOLD:
|
||||||
|
if (onClickHold) onClickHold(data);
|
||||||
|
else openActionModal(ModuleAction.HOLD, data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`[ActionHandler] Unhandled action key: ${action}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
navigation,
|
||||||
|
onClickView,
|
||||||
|
onClickEdit,
|
||||||
|
onClickDuplicate,
|
||||||
|
onClickDelete,
|
||||||
|
onClickActivate,
|
||||||
|
onClickDeactivate,
|
||||||
|
onClickConfirm,
|
||||||
|
onClickCancel,
|
||||||
|
onClickRollback,
|
||||||
|
onClickHold,
|
||||||
|
openActionModal,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Derived State & Configuration
|
// Derived State & Configuration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Determine the number of rows per page based on metadata, defaulting to 10
|
// Determine the number of rows per page based on metadata, defaulting to 10
|
||||||
const perPage = useMemo(() => {
|
const perPage = useMemo(() => {
|
||||||
console.log({ metaData });
|
|
||||||
return metaData?.limit ?? 10;
|
return metaData?.limit ?? 10;
|
||||||
}, [metaData]);
|
}, [metaData]);
|
||||||
|
|
||||||
@@ -108,23 +339,91 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
|
|
||||||
// Ensure column definitions are referentially stable
|
// Ensure column definitions are referentially stable
|
||||||
const finalColumnDefs = useMemo<ColDef<E>[]>(() => {
|
const finalColumnDefs = useMemo<ColDef<E>[]>(() => {
|
||||||
const masterDetailColumn: ColDef<E> = { maxWidth: 50, sortable: false, cellRenderer: 'agGroupCellRenderer' };
|
// Dedicated Checkbox Column (Pinned to the far left)
|
||||||
const statusColumn: ColDef<E> = {
|
const selectionColumn: ColDef<any> = {
|
||||||
maxWidth: 130,
|
colId: 'selection_column',
|
||||||
field: 'status' as any,
|
maxWidth: 40,
|
||||||
headerName: t('common:fields.status'),
|
pinned: 'left',
|
||||||
cellRenderer: ({ value }: any) => <StatusBadge status={value} />,
|
sortable: false,
|
||||||
|
filter: false,
|
||||||
|
suppressHeaderMenuButton: true,
|
||||||
|
checkboxSelection: true, // Manually enable checkbox selection specifically for this column
|
||||||
|
suppressMovable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const prefixColumn: ColDef<E>[] = [props.masterDetail ? masterDetailColumn : (null as any), statusColumn].filter(
|
// Define the Master-Detail collapse/expand column
|
||||||
Boolean,
|
const masterDetailColumn: ColDef<E> = {
|
||||||
);
|
colId: 'master_detail_column',
|
||||||
|
pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling
|
||||||
|
maxWidth: 50,
|
||||||
|
|
||||||
return [...prefixColumn, ...columnDefs];
|
sortable: false, // Disable sorting
|
||||||
}, [columnDefs, props.masterDetail]);
|
filter: false, // Disable filtering
|
||||||
|
suppressHeaderMenuButton: true, // Suppress menu to keep the header clean
|
||||||
|
suppressMovable: true,
|
||||||
|
|
||||||
|
cellRenderer: 'agGroupCellRenderer',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the Action column (for Edit, Delete, View, etc.)
|
||||||
|
const actionColumn: ColDef<any> = {
|
||||||
|
colId: 'action_column',
|
||||||
|
pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling,
|
||||||
|
width: 180,
|
||||||
|
minWidth: 100,
|
||||||
|
sortable: false, // Disable sorting
|
||||||
|
filter: false, // Disable filtering
|
||||||
|
suppressHeaderMenuButton: true, // Suppress menu to keep the header clean
|
||||||
|
suppressSizeToFit: true, // Prevent this column from stretching if you call api.sizeColumnsToFit()
|
||||||
|
suppressMovable: true,
|
||||||
|
|
||||||
|
headerName: t('common:fields.action'),
|
||||||
|
cellRenderer: (params: any) => {
|
||||||
|
if (!params.data) return null;
|
||||||
|
return (
|
||||||
|
<RowActionMenu
|
||||||
|
data={params.data}
|
||||||
|
onActionClick={handleActionClick}
|
||||||
|
statusKey={statusKey}
|
||||||
|
customActions={customRowActions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the default Status column
|
||||||
|
const statusColumn: ColDef<E> = {
|
||||||
|
colId: 'status',
|
||||||
|
maxWidth: 130,
|
||||||
|
|
||||||
|
suppressHeaderMenuButton: true, // Suppress menu to keep the header clean
|
||||||
|
|
||||||
|
field: 'status' as any,
|
||||||
|
headerName: t('common:fields.status'),
|
||||||
|
cellRenderer: ({ value }: any) => <StatusBadge status={value} size="md" variant="outline" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefixColumn: ColDef<E>[] = [
|
||||||
|
selectionColumn,
|
||||||
|
props.masterDetail ? masterDetailColumn : (null as any),
|
||||||
|
actionColumn,
|
||||||
|
statusColumn,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return [...(customPrefixColumn ? customPrefixColumn(prefixColumn) : prefixColumn), ...columnDefs];
|
||||||
|
}, [
|
||||||
|
columnDefs,
|
||||||
|
props.masterDetail,
|
||||||
|
isTransaction,
|
||||||
|
customPrefixColumn,
|
||||||
|
t,
|
||||||
|
statusKey,
|
||||||
|
customRowActions,
|
||||||
|
handleActionClick,
|
||||||
|
]);
|
||||||
|
|
||||||
// Default configuration applied to all columns in the grid
|
// Default configuration applied to all columns in the grid
|
||||||
const defaultColDef = useMemo<ColDef>(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []);
|
const defaultColDef = useMemo<ColDef>(() => ({ flex: 1, minWidth: 40, sortable: true, resizable: true }), []);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Data Source (Server-Side Row Model)
|
// Data Source (Server-Side Row Model)
|
||||||
@@ -240,7 +539,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
columnDefs={finalColumnDefs}
|
columnDefs={finalColumnDefs}
|
||||||
defaultColDef={defaultColDef}
|
defaultColDef={defaultColDef}
|
||||||
animateRows={true}
|
animateRows={true}
|
||||||
rowSelection={{ mode: 'multiRow', checkboxes: true, copySelectedRows: false, headerCheckbox: false }}
|
rowSelection={{ mode: 'multiRow', checkboxes: false, copySelectedRows: false, headerCheckbox: false }}
|
||||||
enableCellTextSelection={true}
|
enableCellTextSelection={true}
|
||||||
onSelectionChanged={handleSelectionChanged}
|
onSelectionChanged={handleSelectionChanged}
|
||||||
onGridReady={onGridReady}
|
onGridReady={onGridReady}
|
||||||
@@ -252,6 +551,14 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
{...restAgGridProps}
|
{...restAgGridProps}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Action Confirmation Modal */}
|
||||||
|
<ActionConfirmationModal<E>
|
||||||
|
modalState={actionModalState}
|
||||||
|
onClose={closeActionModal}
|
||||||
|
onExecute={executeAction}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
|||||||
// Expanded / Full mode
|
// Expanded / Full mode
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
// Helper variable agar kode lebih bersih
|
|
||||||
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0;
|
const hasBreadcrumbs = breadcrumbs && breadcrumbs.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const handleActionClick = useCallback(
|
const handleActionClick = useCallback(
|
||||||
async (key: string) => {
|
async (key: string) => {
|
||||||
// Guard utama: Untuk aksi selain CREATE, pastikan dataId dan detailData sudah ada
|
// Main guard: For actions other than CREATE, make sure dataId and detailData exist.
|
||||||
const hasValidData = Boolean(dataId && detailData);
|
const hasValidData = Boolean(dataId && detailData);
|
||||||
const currentData = detailData as E;
|
const currentData = detailData as E;
|
||||||
|
|
||||||
@@ -356,7 +356,6 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
// Semua dependensi wajib dimasukkan agar terhindar dari bug Stale Closure
|
|
||||||
navigation,
|
navigation,
|
||||||
privileges,
|
privileges,
|
||||||
dataId,
|
dataId,
|
||||||
|
|||||||
Reference in New Issue
Block a user