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:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useCallback, useRef } from 'react';
|
||||
import { useMemo, useCallback, useRef, useState } from 'react';
|
||||
import { AgGridReactProps } from 'ag-grid-react';
|
||||
import {
|
||||
ColDef,
|
||||
@@ -16,12 +16,17 @@ import { Box } from '@mantine/core';
|
||||
import { DataGrid, StatusBadge } from '../../../../components';
|
||||
|
||||
import {
|
||||
useEnterpriseModuleConfigContext,
|
||||
useEnterpriseModuleDataServiceContext,
|
||||
useEnterpriseModuleSelectionContext,
|
||||
useEnterpriseModuleTranslationContext,
|
||||
useEnterpriseModuleNavigationContext,
|
||||
} from '../../hooks/use-module.context';
|
||||
import { BaseEntity } from '@repo/core-api/data-services';
|
||||
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-react';
|
||||
@@ -60,6 +65,30 @@ export interface EnterpriseDataTableProps<E extends BaseEntity> extends Omit<AgG
|
||||
noRowsMessage?: string;
|
||||
|
||||
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...',
|
||||
noRowsMessage = 'No records found',
|
||||
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
|
||||
} = props;
|
||||
|
||||
@@ -86,6 +138,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
||||
const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
|
||||
const navigation = useEnterpriseModuleNavigationContext();
|
||||
|
||||
const { config } = useEnterpriseModuleConfigContext();
|
||||
const { moduleType } = config;
|
||||
const isTransaction = moduleType === 'TRANSACTION';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local UI State
|
||||
@@ -94,12 +151,186 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
// Reference to the AG Grid API for programmatic interaction
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
// Determine the number of rows per page based on metadata, defaulting to 10
|
||||
const perPage = useMemo(() => {
|
||||
console.log({ metaData });
|
||||
return metaData?.limit ?? 10;
|
||||
}, [metaData]);
|
||||
|
||||
@@ -108,23 +339,91 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
|
||||
// Ensure column definitions are referentially stable
|
||||
const finalColumnDefs = useMemo<ColDef<E>[]>(() => {
|
||||
const masterDetailColumn: ColDef<E> = { maxWidth: 50, sortable: false, cellRenderer: 'agGroupCellRenderer' };
|
||||
const statusColumn: ColDef<E> = {
|
||||
maxWidth: 130,
|
||||
field: 'status' as any,
|
||||
headerName: t('common:fields.status'),
|
||||
cellRenderer: ({ value }: any) => <StatusBadge status={value} />,
|
||||
// Dedicated Checkbox Column (Pinned to the far left)
|
||||
const selectionColumn: ColDef<any> = {
|
||||
colId: 'selection_column',
|
||||
maxWidth: 40,
|
||||
pinned: 'left',
|
||||
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(
|
||||
Boolean,
|
||||
);
|
||||
// Define the Master-Detail collapse/expand column
|
||||
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];
|
||||
}, [columnDefs, props.masterDetail]);
|
||||
sortable: false, // Disable sorting
|
||||
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
|
||||
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)
|
||||
@@ -240,7 +539,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
columnDefs={finalColumnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
animateRows={true}
|
||||
rowSelection={{ mode: 'multiRow', checkboxes: true, copySelectedRows: false, headerCheckbox: false }}
|
||||
rowSelection={{ mode: 'multiRow', checkboxes: false, copySelectedRows: false, headerCheckbox: false }}
|
||||
enableCellTextSelection={true}
|
||||
onSelectionChanged={handleSelectionChanged}
|
||||
onGridReady={onGridReady}
|
||||
@@ -252,6 +551,14 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
{...restAgGridProps}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Action Confirmation Modal */}
|
||||
<ActionConfirmationModal<E>
|
||||
modalState={actionModalState}
|
||||
onClose={closeActionModal}
|
||||
onExecute={executeAction}
|
||||
t={t}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user