From f70aa9d29e9733849edfd3c48a28c5c72fbaddb2 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:56:26 +0700 Subject: [PATCH] feat: enhance full page functionality with updated translations, improved pagination handling, and new bulk action components --- .../presentation/languages/en/full-page.json | 4 +- .../presentation/languages/id/full-page.json | 4 +- .../pages/full-page.page.detail.tsx | 2 +- .../pages/full-page.page.index.tsx | 44 +++++------ .../full-page/presentation/store/index.ts | 10 +-- .../data-services/base-data.transformer.ts | 12 +-- .../data-table/components/bulk-actions.tsx | 61 +++++++++++++++ .../data-table/components/row-actions.tsx | 77 +++++++++++++++++++ .../components/data-table/index.tsx | 37 +++++---- 9 files changed, 196 insertions(+), 55 deletions(-) create mode 100644 packages/ui/src/foundations/enterprise-module/components/data-table/components/bulk-actions.tsx create mode 100644 packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx diff --git a/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json index 791cecd..766c70d 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json +++ b/apps/web/src/apps/modules/example/full-page/presentation/languages/en/full-page.json @@ -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" } } \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json b/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json index 5ced49d..2d2b36f 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json +++ b/apps/web/src/apps/modules/example/full-page/presentation/languages/id/full-page.json @@ -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" } } \ No newline at end of file diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx index 9917e82..e467660 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx @@ -6,10 +6,10 @@ export default function FullPagePageDetail() { return ( [] = useMemo(() => { + return [ + { field: 'code', headerName: t('fields.code'), minWidth: 150 }, + { field: 'name', headerName: t('fields.name'), minWidth: 150 }, + ]; + }, [t]); + return ( , - }} - /> - ), - badges: ( - } - style={{ textTransform: 'capitalize' }} - > - Healthy - + }} /> ), + icon: LayoutDashboard, breadcrumbs: [ { label: t('nav:example-module'), type: 'text' }, @@ -40,7 +34,7 @@ export default function FullPagePageIndex() { ], }} > - + ); } diff --git a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts index 55527ad..d347059 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts +++ b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts @@ -5,15 +5,15 @@ import { FullPageEntity } from '../../domain/entities'; export interface FullPageStoreState extends EnterpriseModuleState {} export const fullPageStore = create((set) => ({ - metaData: null, + metaData: { limit: 15 }, setMetaData: (data) => set({ metaData: data }), - - filterData: { page: 1, limit: 10 }, + + filterData: {}, setFilterData: (data) => set({ filterData: data }), - + selectedRows: [], setSelectedRows: (rows) => set({ selectedRows: rows }), - + privileges: [], setPrivileges: (privileges) => set({ privileges }), })); diff --git a/packages/core-api/src/data-services/base-data.transformer.ts b/packages/core-api/src/data-services/base-data.transformer.ts index cf2b869..d58f8bc 100644 --- a/packages/core-api/src/data-services/base-data.transformer.ts +++ b/packages/core-api/src/data-services/base-data.transformer.ts @@ -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 { @@ -21,7 +22,7 @@ export interface PaginatedResponse { 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, }; } diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/components/bulk-actions.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/components/bulk-actions.tsx new file mode 100644 index 0000000..3148b04 --- /dev/null +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/components/bulk-actions.tsx @@ -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: , + variant: 'default', + onClick: () => onActionClick(ModuleAction.DEACTIVATE, selectedRows), + }); + } + + if (hasInactive) { + defaultActions.push({ + key: ModuleAction.ACTIVATE, + label: t('common:actions.activate'), + icon: , + variant: 'default', + onClick: () => onActionClick(ModuleAction.ACTIVATE, selectedRows), + }); + } + + defaultActions.push({ + key: ModuleAction.DELETE, + label: t('common:actions.delete'), + icon: , + variant: 'outline', + intent: 'destructive', + onClick: () => onActionClick(ModuleAction.DELETE, selectedRows), + }); + + return customBulkActions ? customBulkActions(selectedRows, defaultActions) : defaultActions; + }, [selectedRows, statusKey, t, onActionClick, customBulkActions]); +} diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx new file mode 100644 index 0000000..b45ae8f --- /dev/null +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/components/row-actions.tsx @@ -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: , + onClick: () => onActionClick('VIEW' as any, data), + }); + + // Active/Inactive toggle + if (status === 'active') { + actions.push({ + key: ModuleAction.DEACTIVATE, + label: t('common:actions.deactivate'), + icon: , + onClick: () => onActionClick(ModuleAction.DEACTIVATE, data), + }); + } else if (status === 'inactive') { + actions.push({ + key: ModuleAction.ACTIVATE, + label: t('common:actions.activate'), + icon: , + onClick: () => onActionClick(ModuleAction.ACTIVATE, data), + }); + } + + // Delete + actions.push({ + key: ModuleAction.DELETE, + label: t('common:actions.delete'), + icon: , + color: 'red', + onClick: () => onActionClick(ModuleAction.DELETE, data), + }); + + return actions; + }, [status, t, onActionClick, data]); + + const finalActions = customActions ? customActions(data, defaultActions) : defaultActions; + + return ( + + + + + + + + {finalActions.map((action, idx) => ( + + {action.label} + + ))} + + + ); +} diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index a50b27f..7cb7ddd 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -7,7 +7,6 @@ import { IServerSideDatasource, IServerSideGetRowsParams, SelectionChangedEvent, - GetContextMenuItemsParams, MenuItemDef, DefaultMenuItem, StatusBar, @@ -86,7 +85,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT // --------------------------------------------------------------------------- const { t } = useEnterpriseModuleTranslationContext(); const { dataServices } = useEnterpriseModuleDataServiceContext(); - const { setSelectedRows, setFilterData, metaData, setMetaData } = useEnterpriseModuleSelectionContext(); + const { setSelectedRows, metaData, setMetaData } = useEnterpriseModuleSelectionContext(); // --------------------------------------------------------------------------- // Local UI State @@ -100,6 +99,7 @@ export function EnterpriseDataTable(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(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(() => { 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 ( {/* GRID CONTAINER */} - + 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(props: EnterpriseDataT onGridReady={onGridReady} getContextMenuItems={getContextMenuItems} statusBar={statusBar} + domLayout={domLayout} overlayLoadingTemplate={`${loadingMessage}`} overlayNoRowsTemplate={`${noRowsMessage}`} {...restAgGridProps}