feat: enhance full page functionality with updated translations, improved pagination handling, and new bulk action components

This commit is contained in:
Firman Ramdhani
2026-07-22 10:56:26 +07:00
parent 058a571866
commit f70aa9d29e
9 changed files with 196 additions and 55 deletions
@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import { ModuleAction, ModuleActionType } from '../../../entities/entity';
import { useEnterpriseModuleTranslationContext } from '../../../hooks/use-module.context';
import { Trash2, CheckCircle, XCircle } from 'lucide-react';
import { PageActionProps } from '../../../../../components';
export interface UseBulkActionsProps {
selectedRows: any[];
onActionClick: (action: ModuleActionType, data: any[]) => void;
statusKey?: string;
customBulkActions?: (selectedRows: any[], defaultActions: PageActionProps[]) => PageActionProps[];
}
export function useBulkActions({
selectedRows,
onActionClick,
statusKey = 'status',
customBulkActions,
}: UseBulkActionsProps) {
const { t } = useEnterpriseModuleTranslationContext();
return useMemo(() => {
if (!selectedRows || selectedRows.length === 0) return [];
const hasActive = selectedRows.some((row) => row[statusKey]?.toLowerCase() === 'active');
const hasInactive = selectedRows.some((row) => row[statusKey]?.toLowerCase() === 'inactive');
const defaultActions: PageActionProps[] = [];
if (hasActive) {
defaultActions.push({
key: ModuleAction.DEACTIVATE,
label: t('common:actions.deactivate'),
icon: <XCircle size={16} />,
variant: 'default',
onClick: () => onActionClick(ModuleAction.DEACTIVATE, selectedRows),
});
}
if (hasInactive) {
defaultActions.push({
key: ModuleAction.ACTIVATE,
label: t('common:actions.activate'),
icon: <CheckCircle size={16} />,
variant: 'default',
onClick: () => onActionClick(ModuleAction.ACTIVATE, selectedRows),
});
}
defaultActions.push({
key: ModuleAction.DELETE,
label: t('common:actions.delete'),
icon: <Trash2 size={16} />,
variant: 'outline',
intent: 'destructive',
onClick: () => onActionClick(ModuleAction.DELETE, selectedRows),
});
return customBulkActions ? customBulkActions(selectedRows, defaultActions) : defaultActions;
}, [selectedRows, statusKey, t, onActionClick, customBulkActions]);
}
@@ -0,0 +1,77 @@
import { useMemo } from 'react';
import { Menu, ActionIcon } from '@mantine/core';
import { MoreVertical, Eye, Trash2, CheckCircle, XCircle } from 'lucide-react';
import { ModuleAction, ModuleActionType } from '../../../entities/entity';
import { useEnterpriseModuleTranslationContext } from '../../../hooks/use-module.context';
export interface RowActionMenuProps {
data: any;
rowIndex: number;
onActionClick: (action: ModuleActionType, data: any) => void;
statusKey?: string;
customActions?: (data: any, defaultActions: any[]) => any[];
}
export function RowActionMenu({ data, onActionClick, statusKey = 'status', customActions }: RowActionMenuProps) {
const { t } = useEnterpriseModuleTranslationContext();
const status = data?.[statusKey]?.toLowerCase();
const defaultActions = useMemo(() => {
const actions: any[] = [];
// View Details
actions.push({
key: 'VIEW',
label: t('common:actions.detail'),
icon: <Eye size={14} />,
onClick: () => onActionClick('VIEW' as any, data),
});
// Active/Inactive toggle
if (status === 'active') {
actions.push({
key: ModuleAction.DEACTIVATE,
label: t('common:actions.deactivate'),
icon: <XCircle size={14} />,
onClick: () => onActionClick(ModuleAction.DEACTIVATE, 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
actions.push({
key: ModuleAction.DELETE,
label: t('common:actions.delete'),
icon: <Trash2 size={14} />,
color: 'red',
onClick: () => onActionClick(ModuleAction.DELETE, data),
});
return actions;
}, [status, t, onActionClick, data]);
const finalActions = customActions ? customActions(data, defaultActions) : defaultActions;
return (
<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>
);
}
@@ -7,7 +7,6 @@ import {
IServerSideDatasource,
IServerSideGetRowsParams,
SelectionChangedEvent,
GetContextMenuItemsParams,
MenuItemDef,
DefaultMenuItem,
StatusBar,
@@ -86,7 +85,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
// ---------------------------------------------------------------------------
const { t } = useEnterpriseModuleTranslationContext();
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
const { setSelectedRows, setFilterData, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
// ---------------------------------------------------------------------------
// Local UI State
@@ -100,6 +99,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
// ---------------------------------------------------------------------------
// Determine the number of rows per page based on metadata, defaulting to 10
const perPage = useMemo(() => {
console.log({ metaData });
return metaData?.limit ?? 10;
}, [metaData]);
@@ -186,35 +186,45 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
);
// Configures the context menu items available when right-clicking a cell
const getContextMenuItems = useCallback(
(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
params: GetContextMenuItemsParams,
): (DefaultMenuItem | MenuItemDef)[] | Promise<(DefaultMenuItem | MenuItemDef)[]> => {
return ['copy', 'copyWithHeaders'];
},
[],
);
const getContextMenuItems = useCallback(():
| (DefaultMenuItem | MenuItemDef)[]
| Promise<(DefaultMenuItem | MenuItemDef)[]> => {
return ['copy', 'copyWithHeaders'];
}, []);
const statusBar = useMemo<StatusBar | undefined>(() => {
if (!showStatusbar) return undefined;
return { statusPanels: [{ statusPanel: 'agSelectedRowCountComponent', align: 'left' }] };
}, [showStatusbar]);
const domLayout = useMemo(() => {
const meta = { total: metaData?.total ?? 0, limit: metaData?.limit ?? 0 };
if (!isPaginated) {
return meta?.total < meta?.limit ? 'autoHeight' : 'normal';
}
return 'autoHeight';
}, [metaData, isPaginated]);
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<Box>
{/* GRID CONTAINER */}
<Box style={{ height: gridHeight, width: '100%' }} className="erp-data-grid-container">
<Box
className="erp-data-grid-container"
style={{
width: '100%',
height: domLayout === 'autoHeight' || restAgGridProps.domLayout === 'autoHeight' ? '100%' : gridHeight,
}}
>
<DataGrid<E>
rowModelType="serverSide"
getRowId={(v) => v.data.id as string}
cacheBlockSize={perPage}
pagination={isPaginated}
paginationPageSize={isPaginated ? perPage : undefined}
paginationPageSizeSelector={isPaginated ? [10, 20, 50] : undefined}
paginationPageSizeSelector={isPaginated ? [10, 15, 20, 50] : undefined}
columnDefs={finalColumnDefs}
defaultColDef={defaultColDef}
animateRows={true}
@@ -224,6 +234,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
onGridReady={onGridReady}
getContextMenuItems={getContextMenuItems}
statusBar={statusBar}
domLayout={domLayout}
overlayLoadingTemplate={`<span style="padding:10px">${loadingMessage}</span>`}
overlayNoRowsTemplate={`<span style="padding:10px;color:var(--ag-foreground-color,#868e96)">${noRowsMessage}</span>`}
{...restAgGridProps}