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 1/2] 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} From 9a1422ef28952070fdb54c76c881e1097cb837bd Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:42:28 +0700 Subject: [PATCH 2/2] feat: add fields translations to common.json and enhance EnterpriseDataTable with status column and StatusBadge component --- .../core-i18n/src/languages/en/common.json | 35 +++++++++++++++++++ .../core-i18n/src/languages/id/common.json | 35 +++++++++++++++++++ .../components/data-table/index.tsx | 18 ++++++++-- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/core-i18n/src/languages/en/common.json b/packages/core-i18n/src/languages/en/common.json index 9172a9c..98e6a83 100644 --- a/packages/core-i18n/src/languages/en/common.json +++ b/packages/core-i18n/src/languages/en/common.json @@ -113,6 +113,41 @@ "system_menu": { "history": "History", "bookmark": "Bookmark" + }, + "fields": { + "id": "ID", + "code": "Code", + "name": "Name", + "description": "Description", + "number": "Number", + "status": "Status", + "type": "Type", + "category": "Category", + "action": "Action", + "createdAt": "Created At", + "updatedAt": "Updated At", + "createdBy": "Created By", + "updatedBy": "Updated By", + "deletedAt": "Deleted At", + "date": "Date", + "amount": "Amount", + "price": "Price", + "quantity": "Quantity", + "total": "Total", + "balance": "Balance", + "address": "Address", + "phone": "Phone Number", + "email": "Email", + "user": "User", + "role": "Role", + "notes": "Notes", + "isActive": "Active", + "isDefault": "Default", + "isRequired": "Required", + "startDate": "Start Date", + "endDate": "End Date", + "attachment": "Attachment", + "reference": "Reference" } } } \ No newline at end of file diff --git a/packages/core-i18n/src/languages/id/common.json b/packages/core-i18n/src/languages/id/common.json index 2f54f0f..a390063 100644 --- a/packages/core-i18n/src/languages/id/common.json +++ b/packages/core-i18n/src/languages/id/common.json @@ -113,6 +113,41 @@ "system_menu": { "history": "Riwayat", "bookmark": "Bookmark" + }, + "fields": { + "id": "ID", + "code": "Kode", + "name": "Nama", + "description": "Deskripsi", + "number": "Nomor", + "status": "Status", + "type": "Tipe", + "category": "Kategori", + "action": "Aksi", + "createdAt": "Tanggal Dibuat", + "updatedAt": "Tanggal Diperbarui", + "createdBy": "Dibuat Oleh", + "updatedBy": "Diperbarui Oleh", + "deletedAt": "Tanggal Dihapus", + "date": "Tanggal", + "amount": "Nominal", + "price": "Harga", + "quantity": "Kuantitas", + "total": "Total", + "balance": "Saldo", + "address": "Alamat", + "phone": "No. Telepon", + "email": "Email", + "user": "Pengguna", + "role": "Peran", + "notes": "Catatan", + "isActive": "Aktif", + "isDefault": "Bawaan", + "isRequired": "Wajib", + "startDate": "Tanggal Mulai", + "endDate": "Tanggal Selesai", + "attachment": "Lampiran", + "reference": "Referensi" } } } \ No newline at end of file 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 7cb7ddd..51bccbf 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 @@ -13,7 +13,7 @@ import { } from 'ag-grid-community'; import { Box } from '@mantine/core'; -import { DataGrid } from '../../../../components'; +import { DataGrid, StatusBadge } from '../../../../components'; import { useEnterpriseModuleDataServiceContext, @@ -108,8 +108,20 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Ensure column definitions are referentially stable const finalColumnDefs = useMemo[]>(() => { - return columnDefs; - }, [columnDefs]); + const masterDetailColumn: ColDef = { maxWidth: 50, sortable: false, cellRenderer: 'agGroupCellRenderer' }; + const statusColumn: ColDef = { + maxWidth: 130, + field: 'status' as any, + headerName: t('common:fields.status'), + cellRenderer: ({ value }: any) => , + }; + + const prefixColumn: ColDef[] = [props.masterDetail ? masterDetailColumn : (null as any), statusColumn].filter( + Boolean, + ); + + return [...prefixColumn, ...columnDefs]; + }, [columnDefs, props.masterDetail]); // Default configuration applied to all columns in the grid const defaultColDef = useMemo(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []);