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
@@ -8,9 +8,7 @@
"create_page_description": "Fill out the form below to add a new record to the system.",
"duplicate_page_description": "Copy and modify information from an existing record to quickly create a new one.",
"fields": {
"status": "Status",
"name": "Name",
"code": "Code",
"description": "Description"
"name": "Name"
}
}
@@ -8,9 +8,7 @@
"create_page_description": "Lengkapi formulir di bawah ini untuk menambahkan data baru ke dalam sistem.",
"duplicate_page_description": "Salin dan sesuaikan informasi dari data yang sudah ada untuk mempercepat pembuatan data baru.",
"fields": {
"status": "Status",
"name": "Nama",
"code": "Kode",
"description": "Deskripsi"
"name": "Nama"
}
}
@@ -6,10 +6,10 @@ export default function FullPagePageDetail() {
return (
<EnterpriseDetailPageProvider
highlightDataKey="code"
pageHeaderProps={{
title: t('detail_page_title'),
description: t('detail_page_description'),
// showBadges: false,
breadcrumbs: [
{ label: t('nav:example-module'), type: 'text' },
{ label: t('nav:example-full-page'), type: 'link', href: `${fullPageModuleConfig.webUrl}/index` },
@@ -1,38 +1,32 @@
import { EnterpriseIndexPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
import {
EnterpriseIndexPageProvider,
useEnterpriseModuleTranslationContext,
EnterpriseDataTable,
} from '@repo/ui/foundations';
import { ColDef } from '@repo/ui/components';
import { Trans } from '@repo/core-i18n';
import { Badge, Text } from '@repo/ui/components';
import { Activity, LayoutDashboard } from 'lucide-react';
import { DataTable } from '../components/data-table';
import { Text } from '@repo/ui/components';
import { LayoutDashboard } from 'lucide-react';
import { useMemo } from 'react';
export default function FullPagePageIndex() {
const { t } = useEnterpriseModuleTranslationContext();
const columnDefs: ColDef<any>[] = useMemo(() => {
return [
{ field: 'code', headerName: t('fields.code'), minWidth: 150 },
{ field: 'name', headerName: t('fields.name'), minWidth: 150 },
];
}, [t]);
return (
<EnterpriseIndexPageProvider
pageHeaderProps={{
title: t('title'),
description: (
<Trans
t={t}
i18nKey="description"
components={{
1: <Text span fw={500} c="var(--mantine-color-text)" />,
}}
/>
),
badges: (
<Badge
variant="light"
color="teal"
size="sm"
radius="sm"
fw={600}
leftSection={<Activity size={10} strokeWidth={3} />}
style={{ textTransform: 'capitalize' }}
>
Healthy
</Badge>
<Trans t={t} i18nKey="description" components={{ 1: <Text span fw={500} c="var(--mantine-color-text)" /> }} />
),
icon: LayoutDashboard,
breadcrumbs: [
{ label: t('nav:example-module'), type: 'text' },
@@ -40,7 +34,7 @@ export default function FullPagePageIndex() {
],
}}
>
<DataTable />
<EnterpriseDataTable columnDefs={columnDefs} />
</EnterpriseIndexPageProvider>
);
}
@@ -5,10 +5,10 @@ import { FullPageEntity } from '../../domain/entities';
export interface FullPageStoreState extends EnterpriseModuleState<FullPageEntity> {}
export const fullPageStore = create<FullPageStoreState>((set) => ({
metaData: null,
metaData: { limit: 15 },
setMetaData: (data) => set({ metaData: data }),
filterData: { page: 1, limit: 10 },
filterData: {},
setFilterData: (data) => set({ filterData: data }),
selectedRows: [],
@@ -10,10 +10,11 @@ export interface NestJSPaginationMeta {
}
export interface StandardPaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
page?: number;
limit?: number;
total?: number;
totalPages?: number;
countPerPage?: number;
}
export interface PaginatedResponse<T> {
@@ -21,7 +22,7 @@ export interface PaginatedResponse<T> {
meta: StandardPaginationMeta;
}
export function defaultTransformPaginationMeta(meta: any): StandardPaginationMeta {
export function defaultTransformPaginationMeta(meta: NestJSPaginationMeta): StandardPaginationMeta {
if (!meta) {
return { page: 1, limit: 10, total: 0, totalPages: 0 };
}
@@ -30,6 +31,7 @@ export function defaultTransformPaginationMeta(meta: any): StandardPaginationMet
limit: meta.itemsPerPage ?? meta.limit ?? 10,
total: meta.totalItems ?? meta.total ?? 0,
totalPages: meta.totalPages ?? 0,
countPerPage: meta?.itemCount ?? 0,
};
}
@@ -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)[]> => {
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}