feat: introduce index layout skill for FULL_PAGE index/list tables
- Added a new skill for managing index and list table layouts in the ERP project, detailing layout and data contracts. - Updated project guidelines to reference the new index layout skill. - Introduced rules for page composition, column definitions, audit fields, action column width, and toolbar functionality. - Created utility functions for computing action column width and formatting audit fields, along with corresponding unit tests to ensure reliability. These changes enhance the application by providing a structured approach to index layouts, improving consistency and usability across the ERP project.
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
name: index-layout
|
||||||
|
description: Use whenever building or editing a FULL_PAGE index / list table in the ERP project (master-data lists, transaction lists, system users/privileges, any EnterpriseDataTable index). Governs LAYOUT and table contracts ONLY — page chrome, module columnDefs vs shared prefix/postfix columns, action-column width, audit field names, and the search/filter/reload toolbar. Does not govern colors, fonts, border-radius, or other visual styling. Trigger this any time an index page, list table, row-action column, audit columns, or index toolbar is added or restructured — including when the user says "build a list page", "index table", or "add a refresh button".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Index Layout
|
||||||
|
|
||||||
|
Layout and data contracts for **FULL_PAGE index / list tables**. Visual style (color, weight, radius, shadows) belongs to `@repo/ui`, not this skill.
|
||||||
|
|
||||||
|
Walk these rules in order whenever you add or change an index table.
|
||||||
|
|
||||||
|
## Project chrome (do not rebuild)
|
||||||
|
|
||||||
|
Index routes already render inside `EnterpriseIndexPageProvider` → `EnterpriseDataTable`. That chrome owns:
|
||||||
|
|
||||||
|
- Page header (title, description, breadcrumbs, Create)
|
||||||
|
- Toolbar: search, filter, **reload**
|
||||||
|
- Prefix columns: selection, row actions, status
|
||||||
|
- Postfix columns: Created by, Created at, Updated by, Updated at
|
||||||
|
- Pagination / server-side row model
|
||||||
|
|
||||||
|
**Do not** re-implement the toolbar, action column, status column, or audit columns in a module page. This skill governs the **`columnDefs` you pass in** and the **entity/transformer field names** the shared table reads.
|
||||||
|
|
||||||
|
## 1. Page composition
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<EnterpriseIndexPageProvider pageHeaderProps={{ title, description, icon, breadcrumbs }}>
|
||||||
|
<EnterpriseDataTable columnDefs={columnDefs} filterConfig={filterConfig} />
|
||||||
|
</EnterpriseIndexPageProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `example/full-page`. Do not invent a third index layout.
|
||||||
|
|
||||||
|
## 2. Module `columnDefs` = business fields only
|
||||||
|
|
||||||
|
Module index pages declare **business columns only** (code, name, relations, flags).
|
||||||
|
|
||||||
|
Do **not** redeclare:
|
||||||
|
|
||||||
|
- `action_column` / row actions
|
||||||
|
- `status` (the shared status badge column)
|
||||||
|
- `createdBy` / `createdAt` / `updatedBy` / `updatedAt`
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// GOOD
|
||||||
|
const columnDefs = [
|
||||||
|
{ field: 'username', headerName: t('common:fields.username'), minWidth: 160 },
|
||||||
|
{ field: 'privilege', headerName: t('common:fields.privilege'), valueGetter: ({ data }) => relationLabel(data?.privilege) },
|
||||||
|
];
|
||||||
|
|
||||||
|
// BAD — duplicates shared chrome
|
||||||
|
const columnDefs = [
|
||||||
|
{ colId: 'action_column', width: 180 },
|
||||||
|
{ field: 'created_at', headerName: 'Created at' },
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Audit fields are camelCase
|
||||||
|
|
||||||
|
API and transformers map:
|
||||||
|
|
||||||
|
| Entity field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `createdBy` | actor string, `{ username }`, or uuid |
|
||||||
|
| `createdAt` | unix ms |
|
||||||
|
| `updatedBy` | actor string, `{ username }`, or uuid |
|
||||||
|
| `updatedAt` | unix ms |
|
||||||
|
|
||||||
|
`EnterpriseDataTable` postfix columns bind those names and fall back to legacy snake_case (`creator_name`, `created_at`, `editor_name`, `updated_at`) for leftover local/Pouch rows.
|
||||||
|
|
||||||
|
Empty values render as `-`. Nested actors display `username` || `name` || `id`.
|
||||||
|
|
||||||
|
Do not alias audit fields to snake_case in TrackGo transformers.
|
||||||
|
|
||||||
|
## 4. Action column width is shared
|
||||||
|
|
||||||
|
Row actions stay a single nowrap icon row (`RowActions` with `responsiveView={false}`). Width is computed in `EnterpriseDataTable` from privileges + `moduleType` (`View` always, then Edit / Duplicate / Activate-or-Deactivate or transaction flags / Delete). `flex: 0` so the column does not shrink.
|
||||||
|
|
||||||
|
Do **not** override action column `width` / `minWidth` in module `columnDefs` or `customPrefixColumn` unless the module adds extra custom row actions **and** you widen via `customPrefixColumn` to fit them.
|
||||||
|
|
||||||
|
## 5. Toolbar always includes reload
|
||||||
|
|
||||||
|
The index toolbar is search + filter + reload. Reload must call `gridApi.refreshServerSide({ purge: true })` (same as search/filter) — **not** `window.location.reload()`.
|
||||||
|
|
||||||
|
Label: `common:actions.reload` (“Reload” / “Muat Ulang”). Icon: `RefreshCw`.
|
||||||
|
|
||||||
|
Do not add a second module-level refresh control that duplicates this.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Index page uses `EnterpriseIndexPageProvider` + `EnterpriseDataTable` (no custom table chrome)
|
||||||
|
- [ ] `columnDefs` are business fields only
|
||||||
|
- [ ] Transformer maps `createdAt` / `createdBy` / `updatedAt` / `updatedBy`
|
||||||
|
- [ ] Action column width left to the shared table
|
||||||
|
- [ ] Reload is the shared toolbar button, not a full browser refresh
|
||||||
@@ -177,5 +177,6 @@ See `apps/web/.env.example`. Typical `VITE_*` keys: `VITE_APP_ENV`, `VITE_API_BA
|
|||||||
- `.agents/skills/coding-standards/` — TypeScript/React practices
|
- `.agents/skills/coding-standards/` — TypeScript/React practices
|
||||||
- `.agents/skills/form-layout/` — form layout
|
- `.agents/skills/form-layout/` — form layout
|
||||||
- `.agents/skills/detail-layout/` — detail page layout
|
- `.agents/skills/detail-layout/` — detail page layout
|
||||||
|
- `.agents/skills/index-layout/` — index / list table layout
|
||||||
- `.agents/skills/tdd-workflow/` — TDD
|
- `.agents/skills/tdd-workflow/` — TDD
|
||||||
- `.agents/skills/security-review/` — frontend/Electron security
|
- `.agents/skills/security-review/` — frontend/Electron security
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Reference usage: `apps/showcase` (ui-components, forms, action-tools, shell-demo
|
|||||||
|
|
||||||
Detail **content** layout (section stack, key-value grids, status blocks, tabs for many categories): follow `.agents/skills/detail-layout/SKILL.md` — layout/position only.
|
Detail **content** layout (section stack, key-value grids, status blocks, tabs for many categories): follow `.agents/skills/detail-layout/SKILL.md` — layout/position only.
|
||||||
Form **content** layout: follow `.agents/skills/form-layout/SKILL.md`.
|
Form **content** layout: follow `.agents/skills/form-layout/SKILL.md`.
|
||||||
|
Index / list layout (toolbar, action column, audit columns): follow `.agents/skills/index-layout/SKILL.md`.
|
||||||
Entities with `latitude` / `longitude`: follow `web-location-maps.mdc` — form must pick coords on `LocationMap`; detail must render `LocationMap`.
|
Entities with `latitude` / `longitude`: follow `web-location-maps.mdc` — form must pick coords on `LocationMap`; detail must render `LocationMap`.
|
||||||
|
|
||||||
Every page header (`pageHeaderProps`) should include i18n `title`, `description`, `breadcrumbs`, and Lucide `icon` when useful.
|
Every page header (`pageHeaderProps`) should include i18n `title`, `description`, `breadcrumbs`, and Lucide `icon` when useful.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
description: Index / list table contracts for FULL_PAGE modules (actions, audit columns, reload)
|
||||||
|
globs: apps/web/src/apps/**/*.page.index.tsx,packages/ui/src/foundations/enterprise-module/components/data-table/**
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Web Index Table
|
||||||
|
|
||||||
|
Index pages: `EnterpriseIndexPageProvider` + `EnterpriseDataTable`. Do not rebuild the toolbar or default columns. Full layout: `.agents/skills/index-layout/SKILL.md`.
|
||||||
|
|
||||||
|
## Module columnDefs
|
||||||
|
|
||||||
|
Business fields only. Do **not** redeclare action, status, or audit columns.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// GOOD
|
||||||
|
<EnterpriseDataTable columnDefs={[{ field: 'code' }, { field: 'name' }]} filterConfig={filterConfig} />
|
||||||
|
|
||||||
|
// BAD — duplicates shared prefix/postfix
|
||||||
|
columnDefs={[{ colId: 'action_column', width: 180 }, { field: 'created_at' }]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit fields
|
||||||
|
|
||||||
|
Transformers map camelCase: `createdBy`, `createdAt`, `updatedBy`, `updatedAt`. The shared table postfix binds those names (with snake_case fallback). Do not emit `creator_name` / `created_at` in TrackGo transformers.
|
||||||
|
|
||||||
|
## Action column
|
||||||
|
|
||||||
|
Width is computed in `EnterpriseDataTable` from privileges (`flex: 0`, nowrap icons). Do not hard-code `width: 180` in module pages.
|
||||||
|
|
||||||
|
## Reload
|
||||||
|
|
||||||
|
Toolbar includes search, filter, and reload. Reload calls `gridApi.refreshServerSide({ purge: true })` — never `window.location.reload()`. i18n: `common:actions.reload`.
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { defaultPrivileges, noPrivileges } from '../../constant/default-privilege';
|
||||||
|
import { ACTION_COLUMN_CELL_PADDING, ACTION_ICON_WIDTH, computeActionColumnWidth } from './action-column.utils';
|
||||||
|
|
||||||
|
describe('computeActionColumnWidth', () => {
|
||||||
|
it('sizes for View only when no mutating privileges are granted', () => {
|
||||||
|
expect(
|
||||||
|
computeActionColumnWidth({
|
||||||
|
moduleType: 'MASTER_DATA',
|
||||||
|
privileges: noPrivileges,
|
||||||
|
}),
|
||||||
|
).toBe(ACTION_ICON_WIDTH + ACTION_COLUMN_CELL_PADDING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grows with MASTER_DATA privileges (View, Edit, Duplicate, Activate/Deactivate, Delete)', () => {
|
||||||
|
expect(
|
||||||
|
computeActionColumnWidth({
|
||||||
|
moduleType: 'MASTER_DATA',
|
||||||
|
privileges: defaultPrivileges,
|
||||||
|
}),
|
||||||
|
).toBe(5 * ACTION_ICON_WIDTH + ACTION_COLUMN_CELL_PADDING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats activate and deactivate as a single slot on MASTER_DATA', () => {
|
||||||
|
const activateOnly = computeActionColumnWidth({
|
||||||
|
moduleType: 'MASTER_DATA',
|
||||||
|
privileges: { ...noPrivileges, ALLOW_ACTIVATE: true },
|
||||||
|
});
|
||||||
|
const both = computeActionColumnWidth({
|
||||||
|
moduleType: 'MASTER_DATA',
|
||||||
|
privileges: { ...noPrivileges, ALLOW_ACTIVATE: true, ALLOW_DEACTIVATE: true },
|
||||||
|
});
|
||||||
|
expect(activateOnly).toBe(both);
|
||||||
|
expect(activateOnly).toBe(2 * ACTION_ICON_WIDTH + ACTION_COLUMN_CELL_PADDING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('grows with TRANSACTION privileges (View, Edit, Duplicate, Hold, Rollback, Cancel, Confirm, Delete)', () => {
|
||||||
|
expect(
|
||||||
|
computeActionColumnWidth({
|
||||||
|
moduleType: 'TRANSACTION',
|
||||||
|
privileges: defaultPrivileges,
|
||||||
|
}),
|
||||||
|
).toBe(8 * ACTION_ICON_WIDTH + ACTION_COLUMN_CELL_PADDING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is wider for a full TRANSACTION action set than MASTER_DATA', () => {
|
||||||
|
const master = computeActionColumnWidth({
|
||||||
|
moduleType: 'MASTER_DATA',
|
||||||
|
privileges: defaultPrivileges,
|
||||||
|
});
|
||||||
|
const transaction = computeActionColumnWidth({
|
||||||
|
moduleType: 'TRANSACTION',
|
||||||
|
privileges: defaultPrivileges,
|
||||||
|
});
|
||||||
|
expect(transaction).toBeGreaterThan(master);
|
||||||
|
});
|
||||||
|
});
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
import type { PrivilegeEntity } from '../../entities/entity';
|
||||||
|
|
||||||
|
export const ACTION_ICON_WIDTH = 42;
|
||||||
|
export const ACTION_COLUMN_CELL_PADDING = 24;
|
||||||
|
|
||||||
|
export interface ComputeActionColumnWidthParams {
|
||||||
|
moduleType: string;
|
||||||
|
privileges: PrivilegeEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeActionColumnWidth(params: ComputeActionColumnWidthParams): number {
|
||||||
|
const { moduleType, privileges } = params;
|
||||||
|
const isTransaction = moduleType === 'TRANSACTION';
|
||||||
|
|
||||||
|
let actionCount = 1;
|
||||||
|
|
||||||
|
if (privileges.ALLOW_EDIT) actionCount += 1;
|
||||||
|
if (privileges.ALLOW_CREATE) actionCount += 1;
|
||||||
|
|
||||||
|
if (isTransaction) {
|
||||||
|
if (privileges.ALLOW_HOLD) actionCount += 1;
|
||||||
|
if (privileges.ALLOW_ROLLBACK) actionCount += 1;
|
||||||
|
if (privileges.ALLOW_CANCEL) actionCount += 1;
|
||||||
|
if (privileges.ALLOW_CONFIRM) actionCount += 1;
|
||||||
|
} else if (privileges.ALLOW_ACTIVATE || privileges.ALLOW_DEACTIVATE) {
|
||||||
|
actionCount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privileges.ALLOW_DELETE) actionCount += 1;
|
||||||
|
|
||||||
|
return actionCount * ACTION_ICON_WIDTH + ACTION_COLUMN_CELL_PADDING;
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { DateUtils } from '@repo/utils';
|
||||||
|
import {
|
||||||
|
EMPTY_AUDIT_DISPLAY,
|
||||||
|
formatAuditActor,
|
||||||
|
formatAuditTimestamp,
|
||||||
|
resolveAuditValue,
|
||||||
|
} from './audit-column.utils';
|
||||||
|
|
||||||
|
describe('resolveAuditValue', () => {
|
||||||
|
it('prefers camelCase over snake_case', () => {
|
||||||
|
expect(
|
||||||
|
resolveAuditValue({ createdBy: 'alice', creator_name: 'legacy' }, 'createdBy', 'creator_name'),
|
||||||
|
).toBe('alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to snake_case when camelCase is missing', () => {
|
||||||
|
expect(resolveAuditValue({ creator_name: 'legacy' }, 'createdBy', 'creator_name')).toBe('legacy');
|
||||||
|
expect(resolveAuditValue({ created_at: 1_700_000_000_000 }, 'createdAt', 'created_at')).toBe(
|
||||||
|
1_700_000_000_000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined when neither field is present', () => {
|
||||||
|
expect(resolveAuditValue({}, 'createdBy', 'creator_name')).toBeUndefined();
|
||||||
|
expect(resolveAuditValue(undefined, 'createdBy', 'creator_name')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAuditActor', () => {
|
||||||
|
it('returns a string actor as-is', () => {
|
||||||
|
expect(formatAuditActor('alice')).toBe('alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers username, then name, then id on nested actors', () => {
|
||||||
|
expect(formatAuditActor({ username: 'alice', name: 'Alice', id: 'u-1' })).toBe('alice');
|
||||||
|
expect(formatAuditActor({ name: 'Alice', id: 'u-1' })).toBe('Alice');
|
||||||
|
expect(formatAuditActor({ id: 'u-1' })).toBe('u-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a dash when the actor is empty', () => {
|
||||||
|
expect(formatAuditActor(undefined)).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
expect(formatAuditActor(null)).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
expect(formatAuditActor('')).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
expect(formatAuditActor({})).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAuditTimestamp', () => {
|
||||||
|
it('formats unix milliseconds with DateUtils', () => {
|
||||||
|
const ms = 1_700_000_000_000;
|
||||||
|
expect(formatAuditTimestamp(ms)).toBe(new DateUtils(ms).format('DD-MM-YYYY, HH:mm'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts numeric strings', () => {
|
||||||
|
const ms = '1700000000000';
|
||||||
|
expect(formatAuditTimestamp(ms)).toBe(new DateUtils(Number(ms)).format('DD-MM-YYYY, HH:mm'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a dash when the timestamp is empty', () => {
|
||||||
|
expect(formatAuditTimestamp(undefined)).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
expect(formatAuditTimestamp(null)).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
expect(formatAuditTimestamp('')).toBe(EMPTY_AUDIT_DISPLAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
import { DateUtils } from '@repo/utils';
|
||||||
|
|
||||||
|
export const EMPTY_AUDIT_DISPLAY = '-';
|
||||||
|
|
||||||
|
const AUDIT_TIMESTAMP_FORMAT = 'DD-MM-YYYY, HH:mm';
|
||||||
|
|
||||||
|
export function resolveAuditValue(
|
||||||
|
row: Record<string, unknown> | null | undefined,
|
||||||
|
camelKey: string,
|
||||||
|
snakeKey: string,
|
||||||
|
): unknown {
|
||||||
|
if (!row) return undefined;
|
||||||
|
const camelValue = row[camelKey];
|
||||||
|
if (camelValue !== undefined && camelValue !== null) return camelValue;
|
||||||
|
return row[snakeKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAuditActor(value: unknown): string {
|
||||||
|
if (value === undefined || value === null || value === '') return EMPTY_AUDIT_DISPLAY;
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const actor = value as { username?: unknown; name?: unknown; id?: unknown };
|
||||||
|
const label = actor.username ?? actor.name ?? actor.id;
|
||||||
|
if (label === undefined || label === null || label === '') return EMPTY_AUDIT_DISPLAY;
|
||||||
|
return String(label);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAuditTimestamp(value: unknown): string {
|
||||||
|
if (value === undefined || value === null || value === '') return EMPTY_AUDIT_DISPLAY;
|
||||||
|
return new DateUtils(Number(value)).format(AUDIT_TIMESTAMP_FORMAT);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { useDisclosure } from '@mantine/hooks';
|
|||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
Filter,
|
||||||
|
RefreshCw,
|
||||||
// Settings
|
// Settings
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -50,7 +51,8 @@ import { BulkActionConfirmationModal } from '../bulk-action-confirmation';
|
|||||||
import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-drawer';
|
import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-drawer';
|
||||||
// import { TableSettingDrawer } from './components/table-setting-drawer';
|
// import { TableSettingDrawer } from './components/table-setting-drawer';
|
||||||
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
||||||
import { DateUtils } from '@repo/utils';
|
import { computeActionColumnWidth } from './action-column.utils';
|
||||||
|
import { formatAuditActor, formatAuditTimestamp, resolveAuditValue } from './audit-column.utils';
|
||||||
|
|
||||||
export * from 'ag-grid-community';
|
export * from 'ag-grid-community';
|
||||||
export * from 'ag-grid-react';
|
export * from 'ag-grid-react';
|
||||||
@@ -222,9 +224,10 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
} = useEnterpriseModuleSelectionContext<E>();
|
} = useEnterpriseModuleSelectionContext<E>();
|
||||||
const navigation = useEnterpriseModuleNavigationContext();
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
|
||||||
const { config } = useEnterpriseModuleConfigContext();
|
const { config, privileges } = useEnterpriseModuleConfigContext();
|
||||||
const { moduleType } = config;
|
const { moduleType } = config;
|
||||||
const isTransaction = moduleType === 'TRANSACTION';
|
const isTransaction = moduleType === 'TRANSACTION';
|
||||||
|
const actionColumnWidth = computeActionColumnWidth({ moduleType, privileges });
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Local UI State
|
// Local UI State
|
||||||
@@ -295,6 +298,10 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
gridApiRef.current?.refreshServerSide({ purge: true });
|
gridApiRef.current?.refreshServerSide({ purge: true });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleReload = useCallback(() => {
|
||||||
|
gridApiRef.current?.refreshServerSide({ purge: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Action Handlers & Modal State
|
// Action Handlers & Modal State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -647,8 +654,9 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
const actionColumn: ColDef<any> = {
|
const actionColumn: ColDef<any> = {
|
||||||
colId: 'action_column',
|
colId: 'action_column',
|
||||||
pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling,
|
pinned: 'left', // Pin to the right so it remains visible during horizontal scrolling,
|
||||||
width: 180,
|
flex: 0,
|
||||||
minWidth: 100,
|
width: actionColumnWidth,
|
||||||
|
minWidth: actionColumnWidth,
|
||||||
sortable: false, // Disable sorting
|
sortable: false, // Disable sorting
|
||||||
filter: false, // Disable filtering
|
filter: false, // Disable filtering
|
||||||
suppressHeaderMenuButton: true, // Suppress menu to keep the header clean
|
suppressHeaderMenuButton: true, // Suppress menu to keep the header clean
|
||||||
@@ -690,34 +698,36 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
|
|
||||||
const postfixColumn: ColDef<E>[] = [
|
const postfixColumn: ColDef<E>[] = [
|
||||||
{
|
{
|
||||||
colId: 'creator_name',
|
colId: 'createdBy',
|
||||||
field: 'creator_name' as any,
|
field: 'createdBy' as any,
|
||||||
|
minWidth: 160,
|
||||||
headerName: t('common:fields.createdBy'),
|
headerName: t('common:fields.createdBy'),
|
||||||
cellRenderer: ({ value }: any) => value ?? '-',
|
valueGetter: (params) => resolveAuditValue(params.data, 'createdBy', 'creator_name'),
|
||||||
|
cellRenderer: ({ value }: { value: unknown }) => formatAuditActor(value),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
colId: 'created_at',
|
colId: 'createdAt',
|
||||||
field: 'created_at',
|
field: 'createdAt' as any,
|
||||||
|
minWidth: 160,
|
||||||
headerName: t('common:fields.createdAt'),
|
headerName: t('common:fields.createdAt'),
|
||||||
cellRenderer: ({ value }: any) => {
|
valueGetter: (params) => resolveAuditValue(params.data, 'createdAt', 'created_at'),
|
||||||
return value ? new DateUtils(Number(value)).format('DD-MM-YYYY, HH:mm') : '-';
|
cellRenderer: ({ value }: { value: unknown }) => formatAuditTimestamp(value),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colId: 'editor_name',
|
colId: 'updatedBy',
|
||||||
field: 'editor_name' as any,
|
field: 'updatedBy' as any,
|
||||||
|
minWidth: 160,
|
||||||
headerName: t('common:fields.updatedBy'),
|
headerName: t('common:fields.updatedBy'),
|
||||||
cellRenderer: ({ value }: any) => value ?? '-',
|
valueGetter: (params) => resolveAuditValue(params.data, 'updatedBy', 'editor_name'),
|
||||||
|
cellRenderer: ({ value }: { value: unknown }) => formatAuditActor(value),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
colId: 'updated_at',
|
colId: 'updatedAt',
|
||||||
field: 'updated_at',
|
field: 'updatedAt' as any,
|
||||||
|
minWidth: 160,
|
||||||
headerName: t('common:fields.updatedAt'),
|
headerName: t('common:fields.updatedAt'),
|
||||||
cellRenderer: ({ value }: any) => {
|
valueGetter: (params) => resolveAuditValue(params.data, 'updatedAt', 'updated_at'),
|
||||||
return value ? new DateUtils(Number(value)).format('DD-MM-YYYY, HH:mm') : '-';
|
cellRenderer: ({ value }: { value: unknown }) => formatAuditTimestamp(value),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -734,6 +744,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
statusKey,
|
statusKey,
|
||||||
customRowActions,
|
customRowActions,
|
||||||
handleActionClick,
|
handleActionClick,
|
||||||
|
actionColumnWidth,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Default configuration applied to all columns in the grid
|
// Default configuration applied to all columns in the grid
|
||||||
@@ -946,6 +957,14 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
tooltipLabel: t('common:actions.filter'),
|
tooltipLabel: t('common:actions.filter'),
|
||||||
onClick: openFilter,
|
onClick: openFilter,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'reload',
|
||||||
|
icon: <RefreshCw size={16} />,
|
||||||
|
variant: 'default',
|
||||||
|
showLabel: false,
|
||||||
|
tooltipLabel: t('common:actions.reload'),
|
||||||
|
onClick: handleReload,
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// key: 'setting',
|
// key: 'setting',
|
||||||
// icon: <Settings size={16} />,
|
// icon: <Settings size={16} />,
|
||||||
|
|||||||
Reference in New Issue
Block a user