Merge pull request 'core/page-provider' (#26) from core/page-provider into main
Reviewed-on: eigen/fe-monorepo-template#26
This commit is contained in:
+1
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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` },
|
||||
|
||||
+19
-25
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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]);
|
||||
}
|
||||
+77
@@ -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,14 +7,13 @@ import {
|
||||
IServerSideDatasource,
|
||||
IServerSideGetRowsParams,
|
||||
SelectionChangedEvent,
|
||||
GetContextMenuItemsParams,
|
||||
MenuItemDef,
|
||||
DefaultMenuItem,
|
||||
StatusBar,
|
||||
} from 'ag-grid-community';
|
||||
import { Box } from '@mantine/core';
|
||||
|
||||
import { DataGrid } from '../../../../components';
|
||||
import { DataGrid, StatusBadge } from '../../../../components';
|
||||
|
||||
import {
|
||||
useEnterpriseModuleDataServiceContext,
|
||||
@@ -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]);
|
||||
|
||||
@@ -108,8 +108,20 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
|
||||
// Ensure column definitions are referentially stable
|
||||
const finalColumnDefs = useMemo<ColDef<E>[]>(() => {
|
||||
return columnDefs;
|
||||
}, [columnDefs]);
|
||||
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} />,
|
||||
};
|
||||
|
||||
const prefixColumn: ColDef<E>[] = [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<ColDef>(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []);
|
||||
@@ -186,35 +198,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 +246,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}
|
||||
|
||||
Reference in New Issue
Block a user