@pulog/component 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +815 -2
- package/dist/SelectionSummary-N6MMSND2.js +3 -0
- package/dist/SelectionSummary-N6MMSND2.js.map +1 -0
- package/dist/chunk-3JJG43AS.js +213 -0
- package/dist/chunk-3JJG43AS.js.map +1 -0
- package/dist/index.d.ts +550 -0
- package/dist/index.js +3844 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -4
package/README.md
CHANGED
|
@@ -1,3 +1,816 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pulog/component
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shared **business components** for TMS apps. Everything is imported from one place:
|
|
4
|
+
`import { DataTable, ... } from '@pulog/component'`. Each component family lives in its own folder under `src/` and is
|
|
5
|
+
re-exported from `src/index.ts`; adding one (e.g. shipment/order cards) is described in `docs/CONTRIBUTING.md`.
|
|
6
|
+
|
|
7
|
+
Currently available: **DataTable** (this document). Primitives stay in `@pulog/ui`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## DataTable
|
|
12
|
+
|
|
13
|
+
The shared DataTable component family: `DataTable`, its contexts (filter / pagination /
|
|
14
|
+
selection / sort / column visibility / column pinning), filters, pagination, selection helpers,
|
|
15
|
+
query-building utilities (`buildQueryParams`, `applyFilters`, ...) and the default English strings.
|
|
16
|
+
|
|
17
|
+
Extracted from `tms_billing` (the pilot). Source of truth is **this package** - never copy it into an app.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @pulog/component @pulog/ui @pulog/utils
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peer dependencies (the app already has them; the package never bundles them):
|
|
26
|
+
`react`, `react-dom`, `lucide-react`, `date-fns`, `react-day-picker`, `framer-motion`, `react-i18next`.
|
|
27
|
+
`@pulog/ui` and `@pulog/utils` are regular dependencies of this package, but install them in the app too
|
|
28
|
+
when you import from them directly.
|
|
29
|
+
|
|
30
|
+
## App setup (once per app)
|
|
31
|
+
|
|
32
|
+
1. **Tailwind must scan the package.** The package ships compiled JS without CSS, so add to the app's
|
|
33
|
+
`src/index.css` (path relative to that file):
|
|
34
|
+
```css
|
|
35
|
+
@source "../node_modules/@pulog";
|
|
36
|
+
```
|
|
37
|
+
2. **Translations.** Components call `useTranslation('datatable')`. Register the defaults:
|
|
38
|
+
```ts
|
|
39
|
+
import { datatableEn, DATATABLE_NS } from '@pulog/component'
|
|
40
|
+
// add `datatable: datatableEn` to your i18n resources and keep 'datatable' in `ns`
|
|
41
|
+
```
|
|
42
|
+
Override any key by merging your own object over `datatableEn`.
|
|
43
|
+
3. **Portals / Shadow DOM.** Popovers, selects and dropdowns inside the table render through
|
|
44
|
+
`PortalContainerContext` from `@pulog/ui`. The app must provide that context at its Shadow DOM root
|
|
45
|
+
(see `tms_host/REMOTE_STYLE_GUIDE.md`) and must import the context from `@pulog/ui` (or a re-export of it),
|
|
46
|
+
never define its own - two contexts means portals escape the shadow tree.
|
|
47
|
+
|
|
48
|
+
## Exports
|
|
49
|
+
|
|
50
|
+
- Components: `DataTable`, `DataTableProvider`, `SearchFilter`, `DateFilter`, `DateRangeFilter`, `SelectFilter`,
|
|
51
|
+
`MultiSelectFilter`, `CheckboxFilter`, `RangeFilter`, `FilterPanel`, `ColumnHeaderFilter`, `DateTreeFilter`,
|
|
52
|
+
`TraditionalPagination`, `InfiniteScrollPagination`, `RowCheckbox`, `SelectAllCheckbox`, `ExpandableRow`,
|
|
53
|
+
`SelectionSummary`, `ColumnPinMenu`, `ColumnVisibilityPanel`.
|
|
54
|
+
- Context providers/hooks: `FilterProvider/useFilters`, `PaginationProvider/usePagination`,
|
|
55
|
+
`SelectionProvider/useSelection`, `SortProvider/useSort`, `ColumnVisibilityProvider/useColumnVisibility`,
|
|
56
|
+
`ColumnPinningProvider/useColumnPinning`.
|
|
57
|
+
- Types: `DataTableProps`, `DataTableColumn`, `DataTableState` (table level, used by `onStateChange`),
|
|
58
|
+
`DataTableQueryState` (generic query-state shape; this is `DataTableState<T>` from the old `lib/data-table/types`),
|
|
59
|
+
`FilterValue`, `FilterType`, `ColumnFilter`, `SortState`, `PaginationMeta`, `RowSelectionState`, ...
|
|
60
|
+
- Utilities: everything in `src/lib` (`buildQueryParams`, `paramsToURLSearchParams`, `buildURL`, `applyFilters`,
|
|
61
|
+
`removeFilter`, `hasActiveFilters`, `formatDateKey`, `parseDateValue`, ...).
|
|
62
|
+
- i18n: `datatableEn`, `DATATABLE_NS`.
|
|
63
|
+
|
|
64
|
+
`mock-data` (sample users for demos) is intentionally **not** shipped.
|
|
65
|
+
|
|
66
|
+
## Changing this package
|
|
67
|
+
|
|
68
|
+
See the repo-level `docs/CONTRIBUTING.md`. Source: `src/`. Tests: `npm test -w @pulog/component`.
|
|
69
|
+
Known issue: `src/DataTable.test.tsx` is inherited from tms_billing, stale (32 failing cases) and excluded in
|
|
70
|
+
`vitest.config.ts` until it is rewritten.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# Component guide (carried over from tms_billing)
|
|
75
|
+
|
|
76
|
+
Imports below use the package name. Everything else is unchanged from the original guide.
|
|
77
|
+
|
|
78
|
+
# Feature-Rich DataTable Component
|
|
79
|
+
|
|
80
|
+
A comprehensive, production-ready DataTable component built with React 18, TypeScript, and shadcn/ui. Includes advanced filtering, sorting, selection, pagination, and expandable rows.
|
|
81
|
+
|
|
82
|
+
## Features
|
|
83
|
+
|
|
84
|
+
### Filtering System (7 Filter Types)
|
|
85
|
+
- **Search Filter**: Full-text search across fields
|
|
86
|
+
- **Date Filter**: Single date selection
|
|
87
|
+
- **Date Range Filter**: Date range filtering
|
|
88
|
+
- **Select Filter**: Single-select dropdown
|
|
89
|
+
- **Multi-Select Filter**: Multiple selection with badges
|
|
90
|
+
- **Checkbox Filter**: Boolean toggle filtering
|
|
91
|
+
- **Range Filter**: Numeric range slider
|
|
92
|
+
|
|
93
|
+
### Selection & Expansion
|
|
94
|
+
- Row checkboxes with individual selection
|
|
95
|
+
- Select all / deselect all functionality
|
|
96
|
+
- Selection summary with action buttons
|
|
97
|
+
- Expandable rows with custom content renderer
|
|
98
|
+
- Bulk action support for selected rows
|
|
99
|
+
|
|
100
|
+
### Pagination
|
|
101
|
+
- **Traditional Mode**: Page navigation with first/last/prev/next buttons
|
|
102
|
+
- **Infinite Scroll Mode**: Load More button for infinite scrolling
|
|
103
|
+
- Configurable page sizes (10, 25, 50, 100)
|
|
104
|
+
- Total items and current range display
|
|
105
|
+
|
|
106
|
+
### Sorting
|
|
107
|
+
- Multi-column sortable headers
|
|
108
|
+
- Ascending/Descending/None toggle
|
|
109
|
+
- Visual sort indicators
|
|
110
|
+
- Server-side ready query building
|
|
111
|
+
|
|
112
|
+
### Additional Features
|
|
113
|
+
- Top-level optional search bar
|
|
114
|
+
- Scrollable table area with configurable fixed height
|
|
115
|
+
- Sticky table header
|
|
116
|
+
- Sticky columns on the left with automatic offset stacking
|
|
117
|
+
- Column pinning — pin/unpin left or right at runtime via a per-header menu (in addition to a column's static `sticky` config)
|
|
118
|
+
- Column visibility toggle
|
|
119
|
+
- Loading skeleton states
|
|
120
|
+
- Error state handling
|
|
121
|
+
- Empty state message
|
|
122
|
+
- Responsive design
|
|
123
|
+
- Client & server-side ready architecture
|
|
124
|
+
- React Context for state management (no external state library)
|
|
125
|
+
|
|
126
|
+
## Installation & Setup
|
|
127
|
+
|
|
128
|
+
### 1. Basic Setup
|
|
129
|
+
|
|
130
|
+
`<DataTable>` is self-contained — it owns its filter/pagination/selection/sort/
|
|
131
|
+
column-visibility state internally, so there's no separate provider to render.
|
|
132
|
+
Just pass props:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
'use client';
|
|
136
|
+
|
|
137
|
+
import { DataTable } from '@pulog/component';
|
|
138
|
+
|
|
139
|
+
export default function MyPage() {
|
|
140
|
+
return (
|
|
141
|
+
<DataTable
|
|
142
|
+
columns={columns}
|
|
143
|
+
data={data}
|
|
144
|
+
rowKey="id"
|
|
145
|
+
pageSize={10} // initial rows-per-page
|
|
146
|
+
selectionMode="checkbox" // or "radio" — only needed with showSelection
|
|
147
|
+
defaultColumnVisibility={{}} // initial per-column visibility
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`DataTableProvider` still exists (`./contexts/DataTableProvider`) but is an
|
|
154
|
+
internal implementation detail of `<DataTable>` — page code should never need
|
|
155
|
+
to render it directly.
|
|
156
|
+
|
|
157
|
+
**Escape hatch:** the one legitimate reason to render `DataTableProvider`
|
|
158
|
+
yourself is when a *sibling* component (rendered next to `<DataTable>`, not
|
|
159
|
+
passed into it via `toolbarLeft`/`toolbarRight`) needs to read the table's
|
|
160
|
+
selection/filter state — e.g. a "Process selected" button above the table
|
|
161
|
+
that calls `useSelection()`. Wrap both in one explicit `<DataTableProvider>`;
|
|
162
|
+
`<DataTable>` detects that ambient provider and reuses it instead of creating
|
|
163
|
+
a disconnected one of its own:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
function ProcessSelectedButton() {
|
|
167
|
+
const { selectedCount, getSelectedRowIds } = useSelection(); // shares state with the table below
|
|
168
|
+
return <Button disabled={!selectedCount}>Process {selectedCount}</Button>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
<DataTableProvider defaultPageSize={10}>
|
|
172
|
+
<ProcessSelectedButton />
|
|
173
|
+
<DataTable columns={columns} data={data} rowKey="id" pageSize={10} />
|
|
174
|
+
</DataTableProvider>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Keep `defaultPageSize` (and `selectionMode` / `defaultColumnVisibility` if
|
|
178
|
+
used) on the outer `DataTableProvider` in sync with what you'd otherwise pass
|
|
179
|
+
to `<DataTable>` — with an ambient provider present, `<DataTable>`'s own
|
|
180
|
+
`pageSize`/`selectionMode`/`defaultColumnVisibility` props are ignored (the
|
|
181
|
+
ambient provider already owns that state).
|
|
182
|
+
|
|
183
|
+
A nested `<DataTable>` that must stay **isolated** from an outer table (e.g.
|
|
184
|
+
a detail table inside an expanded row, with its own independent pagination)
|
|
185
|
+
should keep its own explicit `<DataTableProvider>` wrapper too — nesting a
|
|
186
|
+
provider always shadows the ambient one for its own subtree, giving it a
|
|
187
|
+
fresh, separate state scope.
|
|
188
|
+
|
|
189
|
+
### 2. Define Column Configuration
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import { DataTableColumn } from '@pulog/component';
|
|
193
|
+
|
|
194
|
+
interface User {
|
|
195
|
+
id: number;
|
|
196
|
+
name: string;
|
|
197
|
+
email: string;
|
|
198
|
+
role: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const columns: DataTableColumn<User>[] = [
|
|
202
|
+
{
|
|
203
|
+
id: 'name',
|
|
204
|
+
header: 'Name',
|
|
205
|
+
accessor: 'name',
|
|
206
|
+
sortable: true,
|
|
207
|
+
filterable: true,
|
|
208
|
+
sticky: 'left', // or `right`, or `true` for left
|
|
209
|
+
filterConfig: {
|
|
210
|
+
id: 'name',
|
|
211
|
+
label: 'Search Name',
|
|
212
|
+
type: 'search',
|
|
213
|
+
placeholder: 'Enter name...',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'email',
|
|
218
|
+
header: 'Email',
|
|
219
|
+
accessor: 'email',
|
|
220
|
+
sortable: true,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'role',
|
|
224
|
+
header: 'Role',
|
|
225
|
+
accessor: 'role',
|
|
226
|
+
sortable: true,
|
|
227
|
+
filterable: true,
|
|
228
|
+
filterConfig: {
|
|
229
|
+
id: 'role',
|
|
230
|
+
label: 'Select Role',
|
|
231
|
+
type: 'multiSelect',
|
|
232
|
+
options: [
|
|
233
|
+
{ label: 'Admin', value: 'admin' },
|
|
234
|
+
{ label: 'User', value: 'user' },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Sticky columns can be added to multiple visible columns. The table measures each sticky column at runtime and stacks their offsets so they do not overlap.
|
|
242
|
+
Use `sticky: 'left'` or `sticky: 'right'` to pin columns on either side by default.
|
|
243
|
+
|
|
244
|
+
### Runtime Column Pinning
|
|
245
|
+
|
|
246
|
+
Each header cell (except tool columns) shows a pin icon that opens a menu with "Pin ke Kiri" / "Pin ke Kanan" / "Lepas Pin" — this lets users pin/unpin any column at runtime, overriding its static `sticky` config for that session. Pinned columns are automatically reordered to the corresponding edge (left-pinned first, then unpinned, then right-pinned), each group keeping its own relative order.
|
|
247
|
+
|
|
248
|
+
- Disable the menu for the whole table: `enableColumnPinning={false}`
|
|
249
|
+
- Disable it for one column: `pinnable: false` on that column
|
|
250
|
+
- Seed initial runtime pin state: `defaultColumnPinning={{ mst_client_id: 'left' }}` (falls back to each column's `sticky` for ids left unset)
|
|
251
|
+
|
|
252
|
+
### 3. Render DataTable
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
<DataTable<User>
|
|
256
|
+
columns={columns}
|
|
257
|
+
data={users}
|
|
258
|
+
rowKey="id"
|
|
259
|
+
showSelection={true}
|
|
260
|
+
showExpand={true}
|
|
261
|
+
expandContent={(row) => <div>Details for {row.name}</div>}
|
|
262
|
+
paginationMode="traditional"
|
|
263
|
+
showFilters={true}
|
|
264
|
+
showSearch={true}
|
|
265
|
+
searchFields={['name', 'email']} // optional, defaults to all columns
|
|
266
|
+
tableHeight={600}
|
|
267
|
+
pageSize={10}
|
|
268
|
+
/>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Usage Examples
|
|
272
|
+
|
|
273
|
+
### Server-Side Integration
|
|
274
|
+
|
|
275
|
+
Enable server-side mode by setting the `manual*` props:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<DataTable
|
|
279
|
+
columns={columns}
|
|
280
|
+
data={data}
|
|
281
|
+
rowKey="id"
|
|
282
|
+
manualPagination
|
|
283
|
+
manualFiltering
|
|
284
|
+
manualSorting
|
|
285
|
+
totalCount={65}
|
|
286
|
+
onStateChange={handleStateChange}
|
|
287
|
+
/>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Using `onStateChange` Callback
|
|
291
|
+
|
|
292
|
+
The recommended approach — `onStateChange` fires whenever pagination, filter, sort, or search state changes:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
const [data, setData] = useState([]);
|
|
296
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
297
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
298
|
+
|
|
299
|
+
const handleStateChange = useCallback(async (state: DataTableState) => {
|
|
300
|
+
setIsLoading(true);
|
|
301
|
+
try {
|
|
302
|
+
const params = buildQueryParams(
|
|
303
|
+
state.pageIndex,
|
|
304
|
+
state.pageSize,
|
|
305
|
+
state.filters,
|
|
306
|
+
state.sort,
|
|
307
|
+
undefined,
|
|
308
|
+
state.search
|
|
309
|
+
);
|
|
310
|
+
const response = await fetch(`/api/data?${paramsToURLSearchParams(params)}`);
|
|
311
|
+
const result = await response.json();
|
|
312
|
+
setData(result.data);
|
|
313
|
+
setTotalCount(result.meta.pagination.totalData);
|
|
314
|
+
} finally {
|
|
315
|
+
setIsLoading(false);
|
|
316
|
+
}
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
<DataTable
|
|
320
|
+
columns={columns}
|
|
321
|
+
data={data}
|
|
322
|
+
isLoading={isLoading}
|
|
323
|
+
rowKey="id"
|
|
324
|
+
manualPagination
|
|
325
|
+
manualFiltering
|
|
326
|
+
manualSorting
|
|
327
|
+
totalCount={totalCount}
|
|
328
|
+
onStateChange={handleStateChange}
|
|
329
|
+
/>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### Reading Table State From a Custom Toolbar Element
|
|
333
|
+
|
|
334
|
+
`useFilters` / `usePagination` / `useSort` / `useSelection` / `useColumnVisibility`
|
|
335
|
+
only work inside `<DataTable>`'s own subtree, since the table creates its own
|
|
336
|
+
context instance internally — a sibling component sitting next to `<DataTable>`
|
|
337
|
+
in JSX can't reach it. Fetching data for the table itself should use
|
|
338
|
+
`onStateChange` (above), not these hooks directly.
|
|
339
|
+
|
|
340
|
+
The one place these hooks are legitimately useful is a **custom toolbar
|
|
341
|
+
element** passed via `toolbarLeft` / `toolbarRight` — that content is rendered
|
|
342
|
+
inside `<DataTable>`'s own tree, so it shares the same context:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
function ActiveDateRangeBadge() {
|
|
346
|
+
// Works: this component is rendered *inside* <DataTable> via toolbarLeft.
|
|
347
|
+
const { filters, removeFilter } = useFilters();
|
|
348
|
+
const active = filters.find((f) => f.field === 'tanggal_valid');
|
|
349
|
+
if (!active) return null;
|
|
350
|
+
return <Badge onClick={() => removeFilter('tanggal_valid')}>Tanggal aktif ✕</Badge>;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
<DataTable
|
|
354
|
+
columns={columns}
|
|
355
|
+
data={data}
|
|
356
|
+
rowKey="id"
|
|
357
|
+
toolbarLeft={<ActiveDateRangeBadge />}
|
|
358
|
+
/>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Custom Cell Rendering
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
const columns: DataTableColumn<User>[] = [
|
|
365
|
+
{
|
|
366
|
+
id: 'status',
|
|
367
|
+
header: 'Status',
|
|
368
|
+
accessor: 'status',
|
|
369
|
+
cell: (row) => (
|
|
370
|
+
<Badge variant={row.status === 'active' ? 'default' : 'secondary'}>
|
|
371
|
+
{row.status}
|
|
372
|
+
</Badge>
|
|
373
|
+
),
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Expandable Rows with Details
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
<DataTable
|
|
382
|
+
columns={columns}
|
|
383
|
+
data={data}
|
|
384
|
+
rowKey="id"
|
|
385
|
+
showExpand={true}
|
|
386
|
+
expandContent={(row) => (
|
|
387
|
+
<div className="space-y-4">
|
|
388
|
+
<div className="grid grid-cols-2 gap-4">
|
|
389
|
+
<div>
|
|
390
|
+
<p className="text-xs text-muted-foreground">Email</p>
|
|
391
|
+
<p className="font-medium">{row.email}</p>
|
|
392
|
+
</div>
|
|
393
|
+
<div>
|
|
394
|
+
<p className="text-xs text-muted-foreground">Phone</p>
|
|
395
|
+
<p className="font-medium">{row.phone}</p>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
<p>{row.description}</p>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
/>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Row Selection with Bulk Actions
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
import { useSelection } from '@pulog/component';
|
|
408
|
+
|
|
409
|
+
function DataTableWithActions() {
|
|
410
|
+
const { getSelectedRowIds } = useSelection();
|
|
411
|
+
|
|
412
|
+
const handleBulkDelete = async () => {
|
|
413
|
+
const selectedIds = getSelectedRowIds();
|
|
414
|
+
await fetch('/api/users/bulk-delete', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
body: JSON.stringify({ ids: selectedIds }),
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<>
|
|
422
|
+
<DataTable
|
|
423
|
+
columns={columns}
|
|
424
|
+
data={data}
|
|
425
|
+
rowKey="id"
|
|
426
|
+
showSelection={true}
|
|
427
|
+
/>
|
|
428
|
+
<SelectionSummary
|
|
429
|
+
actionLabel="Delete Selected"
|
|
430
|
+
onAction={handleBulkDelete}
|
|
431
|
+
/>
|
|
432
|
+
</>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## API Integration Reference
|
|
438
|
+
|
|
439
|
+
### onStateChange Callback Payload
|
|
440
|
+
|
|
441
|
+
When any table state changes, `onStateChange` fires with this structure:
|
|
442
|
+
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"pageIndex": 2,
|
|
446
|
+
"pageSize": 10,
|
|
447
|
+
"sort": {
|
|
448
|
+
"column": "name",
|
|
449
|
+
"order": "asc"
|
|
450
|
+
},
|
|
451
|
+
"filters": [
|
|
452
|
+
{ "field": "status", "type": "select", "operator": "equals", "value": "active" },
|
|
453
|
+
{ "field": "salary", "type": "range", "operator": "between", "value": [50000, 100000] }
|
|
454
|
+
],
|
|
455
|
+
"search": {
|
|
456
|
+
"query": "john",
|
|
457
|
+
"fields": ["name", "email"]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### buildQueryParams Output
|
|
463
|
+
|
|
464
|
+
Use `buildQueryParams()` to convert state into API-friendly query parameters:
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { buildQueryParams, paramsToURLSearchParams } from '@pulog/component';
|
|
468
|
+
|
|
469
|
+
const params = buildQueryParams(pageIndex, pageSize, filters, sort, additionalParams, search);
|
|
470
|
+
const urlParams = paramsToURLSearchParams(params);
|
|
471
|
+
// Use: fetch(`/api/data?${urlParams}`)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### GET Request — Query Parameters
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
GET /api/data?pageIndex=2&pageSize=10&sortBy=name&sortOrder=asc&search=john&searchFields=name&searchFields=email&filters[status]=active&filters[salary][min]=50000&filters[salary][max]=100000
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
Full parameter reference:
|
|
481
|
+
|
|
482
|
+
| Parameter | Type | Example | Description |
|
|
483
|
+
|---|---|---|---|
|
|
484
|
+
| `pageIndex` | number | `2` | Current page (1-indexed) |
|
|
485
|
+
| `pageSize` | number | `10` | Items per page |
|
|
486
|
+
| `sortBy` | string | `name` | Sort column ID |
|
|
487
|
+
| `sortOrder` | string | `asc` | Sort direction: `asc` or `desc` |
|
|
488
|
+
| `search` | string | `john` | Global search query |
|
|
489
|
+
| `searchFields` | string[] | `name,email` | Fields to search in |
|
|
490
|
+
| `filters[field]` | any | `active` | Simple filter value |
|
|
491
|
+
| `filters[field][min]` | number | `50000` | Range filter min |
|
|
492
|
+
| `filters[field][max]` | number | `100000` | Range filter max |
|
|
493
|
+
| `filters[field][from]` | string | `2024-01-01` | Date range start |
|
|
494
|
+
| `filters[field][to]` | string | `2024-12-31` | Date range end |
|
|
495
|
+
| `filters[field][]` | string[] | `admin,user` | Multi-select values |
|
|
496
|
+
|
|
497
|
+
#### POST Request — JSON Body
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
POST /api/data
|
|
501
|
+
Content-Type: application/json
|
|
502
|
+
|
|
503
|
+
{
|
|
504
|
+
"pageIndex": 2,
|
|
505
|
+
"pageSize": 10,
|
|
506
|
+
"sortBy": "name",
|
|
507
|
+
"sortOrder": "asc",
|
|
508
|
+
"search": "john",
|
|
509
|
+
"searchFields": ["name", "email"],
|
|
510
|
+
"filters": {
|
|
511
|
+
"status": "active",
|
|
512
|
+
"role": ["admin", "user"],
|
|
513
|
+
"salary": { "min": 50000, "max": 100000 },
|
|
514
|
+
"joinDate": { "from": "2024-01-01", "to": "2024-12-31" },
|
|
515
|
+
"isVerified": true
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Filter Types → API Format
|
|
521
|
+
|
|
522
|
+
| Filter Type | Value Type | GET Example | POST Example |
|
|
523
|
+
|---|---|---|---|
|
|
524
|
+
| `search` | string | `filters[name]=john` | `"name": "john"` |
|
|
525
|
+
| `date` | string | `filters[date]=2024-06-15` | `"date": "2024-06-15"` |
|
|
526
|
+
| `dateRange` | [from, to] | `filters[dob][from]=2024-01-01&filters[dob][to]=2024-12-31` | `"dob": { "from": "2024-01-01", "to": "2024-12-31" }` |
|
|
527
|
+
| `select` | string | `filters[status]=active` | `"status": "active"` |
|
|
528
|
+
| `multiSelect` | string[] | `filters[role][]=admin&filters[role][]=user` | `"role": ["admin", "user"]` |
|
|
529
|
+
| `checkbox` | boolean | `filters[verified]=true` | `"verified": true` |
|
|
530
|
+
| `range` | [min, max] | `filters[salary][min]=50000&filters[salary][max]=100000` | `"salary": { "min": 50000, "max": 100000 }` |
|
|
531
|
+
|
|
532
|
+
### API Response Format
|
|
533
|
+
|
|
534
|
+
The DataTable expects API responses in this format:
|
|
535
|
+
|
|
536
|
+
```json
|
|
537
|
+
{
|
|
538
|
+
"status": 200,
|
|
539
|
+
"success": true,
|
|
540
|
+
"message": "success",
|
|
541
|
+
"data": [
|
|
542
|
+
{ "id": 1, "name": "User 1", "email": "user1@example.com", "role": "admin" },
|
|
543
|
+
{ "id": 2, "name": "User 2", "email": "user2@example.com", "role": "user" }
|
|
544
|
+
],
|
|
545
|
+
"meta": {
|
|
546
|
+
"pagination": {
|
|
547
|
+
"pageIndex": 2,
|
|
548
|
+
"pageSize": 10,
|
|
549
|
+
"pageCount": 7,
|
|
550
|
+
"totalData": 65
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
#### Response Fields
|
|
557
|
+
|
|
558
|
+
| Field | Type | Required | Description |
|
|
559
|
+
|---|---|---|---|
|
|
560
|
+
| `status` | number | Yes | HTTP status code |
|
|
561
|
+
| `success` | boolean | Yes | Request success flag |
|
|
562
|
+
| `message` | string | Yes | Response message |
|
|
563
|
+
| `data` | array | Yes | Array of row objects |
|
|
564
|
+
| `meta.pagination.pageIndex` | number | Yes | Current page (must match request) |
|
|
565
|
+
| `meta.pagination.pageSize` | number | Yes | Items per page |
|
|
566
|
+
| `meta.pagination.pageCount` | number | Yes | Total number of pages |
|
|
567
|
+
| `meta.pagination.totalData` | number | Yes | Total number of records (across all pages) |
|
|
568
|
+
|
|
569
|
+
### Server-Side Behavior Summary
|
|
570
|
+
|
|
571
|
+
| Feature | Props Required | Behavior |
|
|
572
|
+
|---|---|---|
|
|
573
|
+
| Server pagination | `manualPagination` + `totalCount` | Data not sliced client-side; page count from `totalCount` |
|
|
574
|
+
| Server filtering | `manualFiltering` | Client search/filter skipped; filters sent via `onStateChange` |
|
|
575
|
+
| Server sorting | `manualSorting` | Client sort skipped; sort sent via `onStateChange` |
|
|
576
|
+
| Page reset on filter | `manualFiltering` + `manualPagination` | Auto-reset to page 1 when filters/search/sort change |
|
|
577
|
+
|
|
578
|
+
## Hooks API
|
|
579
|
+
|
|
580
|
+
### useFilters()
|
|
581
|
+
```tsx
|
|
582
|
+
const {
|
|
583
|
+
filters, // Current filter array
|
|
584
|
+
addFilter, // Add a filter
|
|
585
|
+
updateFilter, // Update existing filter
|
|
586
|
+
removeFilter, // Remove filter
|
|
587
|
+
clearFilters, // Clear all filters
|
|
588
|
+
clearFieldFilters, // Clear filters for specific fields
|
|
589
|
+
hasActiveFilters, // Check if any filters active
|
|
590
|
+
getFieldFilters, // Get filters for specific field
|
|
591
|
+
} = useFilters();
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### usePagination()
|
|
595
|
+
```tsx
|
|
596
|
+
const {
|
|
597
|
+
pageIndex, // Current page (1-indexed)
|
|
598
|
+
pageSize, // Items per page
|
|
599
|
+
pageCount, // Total pages
|
|
600
|
+
totalData, // Total items count
|
|
601
|
+
paginationMode, // 'traditional' | 'infinite'
|
|
602
|
+
goToPage, // Jump to specific page
|
|
603
|
+
nextPage, // Go to next page
|
|
604
|
+
previousPage, // Go to previous page
|
|
605
|
+
setPageSize, // Change page size
|
|
606
|
+
updatePaginationMeta, // Update all pagination data
|
|
607
|
+
setPaginationMode, // Switch pagination mode
|
|
608
|
+
canGoNext, // Can go to next?
|
|
609
|
+
canGoPrevious, // Can go to previous?
|
|
610
|
+
} = usePagination();
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### useSelection()
|
|
614
|
+
```tsx
|
|
615
|
+
const {
|
|
616
|
+
rowSelection, // Selected rows object
|
|
617
|
+
expandedRows, // Expanded rows object
|
|
618
|
+
toggleRow, // Toggle row selection
|
|
619
|
+
toggleAll, // Toggle all rows
|
|
620
|
+
selectRows, // Select multiple rows
|
|
621
|
+
deselectRows, // Deselect multiple rows
|
|
622
|
+
clearSelection, // Clear all selections
|
|
623
|
+
isRowSelected, // Check if row selected
|
|
624
|
+
selectedCount, // Count of selected rows
|
|
625
|
+
toggleExpanded, // Toggle row expansion
|
|
626
|
+
isRowExpanded, // Check if row expanded
|
|
627
|
+
expandedCount, // Count of expanded rows
|
|
628
|
+
getSelectedRowIds, // Get array of selected IDs
|
|
629
|
+
} = useSelection();
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### useSort()
|
|
633
|
+
```tsx
|
|
634
|
+
const {
|
|
635
|
+
sort, // { column: string | null, order: 'asc' | 'desc' | null }
|
|
636
|
+
setSort, // Set sort column and order
|
|
637
|
+
toggleSort, // Cycle sort: asc -> desc -> none
|
|
638
|
+
clearSort, // Clear sorting
|
|
639
|
+
isSorted, // Check if column sorted
|
|
640
|
+
getSortOrder, // Get sort order for column
|
|
641
|
+
} = useSort();
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### useColumnVisibility()
|
|
645
|
+
```tsx
|
|
646
|
+
const {
|
|
647
|
+
visibility, // Visibility state object
|
|
648
|
+
toggleColumn, // Toggle column visibility
|
|
649
|
+
showColumn, // Show column
|
|
650
|
+
hideColumn, // Hide column
|
|
651
|
+
isColumnVisible, // Check if visible
|
|
652
|
+
showAllColumns, // Show all columns
|
|
653
|
+
hideAllColumns, // Hide all columns
|
|
654
|
+
setVisibility, // Set all visibility
|
|
655
|
+
} = useColumnVisibility();
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Filter Types Configuration
|
|
659
|
+
|
|
660
|
+
### Search Filter
|
|
661
|
+
```tsx
|
|
662
|
+
{
|
|
663
|
+
id: 'name',
|
|
664
|
+
label: 'Search Name',
|
|
665
|
+
type: 'search',
|
|
666
|
+
placeholder: 'Enter name...',
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Date Filter
|
|
671
|
+
```tsx
|
|
672
|
+
{
|
|
673
|
+
id: 'createdAt',
|
|
674
|
+
label: 'Created Date',
|
|
675
|
+
type: 'date',
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Date Range Filter
|
|
680
|
+
```tsx
|
|
681
|
+
{
|
|
682
|
+
id: 'dateRange',
|
|
683
|
+
label: 'Date Range',
|
|
684
|
+
type: 'dateRange',
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Select Filter
|
|
689
|
+
```tsx
|
|
690
|
+
{
|
|
691
|
+
id: 'status',
|
|
692
|
+
label: 'Status',
|
|
693
|
+
type: 'select',
|
|
694
|
+
options: [
|
|
695
|
+
{ label: 'Active', value: 'active' },
|
|
696
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
697
|
+
],
|
|
698
|
+
placeholder: 'Select status...',
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Multi-Select Filter
|
|
703
|
+
```tsx
|
|
704
|
+
{
|
|
705
|
+
id: 'roles',
|
|
706
|
+
label: 'Roles',
|
|
707
|
+
type: 'multiSelect',
|
|
708
|
+
options: [
|
|
709
|
+
{ label: 'Admin', value: 'admin' },
|
|
710
|
+
{ label: 'User', value: 'user' },
|
|
711
|
+
],
|
|
712
|
+
placeholder: 'Select roles...',
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Checkbox Filter
|
|
717
|
+
```tsx
|
|
718
|
+
{
|
|
719
|
+
id: 'isVerified',
|
|
720
|
+
label: 'Is Verified',
|
|
721
|
+
type: 'checkbox',
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Range Filter
|
|
726
|
+
```tsx
|
|
727
|
+
{
|
|
728
|
+
id: 'salary',
|
|
729
|
+
label: 'Salary Range',
|
|
730
|
+
type: 'range',
|
|
731
|
+
minValue: 0,
|
|
732
|
+
maxValue: 1000000,
|
|
733
|
+
}
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## DataTable Props
|
|
737
|
+
|
|
738
|
+
```tsx
|
|
739
|
+
interface DataTableProps<T> {
|
|
740
|
+
// Required
|
|
741
|
+
columns: DataTableColumn<T>[];
|
|
742
|
+
data: T[];
|
|
743
|
+
rowKey: keyof T;
|
|
744
|
+
|
|
745
|
+
// Display
|
|
746
|
+
isLoading?: boolean;
|
|
747
|
+
error?: string | null;
|
|
748
|
+
onRowClick?: (row: T) => void;
|
|
749
|
+
emptyStateMessage?: string;
|
|
750
|
+
|
|
751
|
+
// Selection & Expand
|
|
752
|
+
showSelection?: boolean;
|
|
753
|
+
showExpand?: boolean;
|
|
754
|
+
stickySelection?: boolean;
|
|
755
|
+
stickyExpand?: boolean;
|
|
756
|
+
showRowNumber?: boolean;
|
|
757
|
+
stickyRowNumber?: boolean;
|
|
758
|
+
expandContent?: (row: T) => React.ReactNode;
|
|
759
|
+
|
|
760
|
+
// Pagination
|
|
761
|
+
paginationMode?: 'traditional' | 'infinite';
|
|
762
|
+
pageSize?: number;
|
|
763
|
+
pageSizeOptions?: number[];
|
|
764
|
+
tableHeight?: string | number;
|
|
765
|
+
infiniteScrollHeight?: string | number;
|
|
766
|
+
|
|
767
|
+
// Search & Filter
|
|
768
|
+
showFilters?: boolean;
|
|
769
|
+
showSearch?: boolean;
|
|
770
|
+
searchFields?: string[];
|
|
771
|
+
searchPlaceholder?: string;
|
|
772
|
+
|
|
773
|
+
// Server-side
|
|
774
|
+
manualPagination?: boolean; // Enable server-side pagination
|
|
775
|
+
manualSorting?: boolean; // Enable server-side sorting
|
|
776
|
+
manualFiltering?: boolean; // Enable server-side filtering
|
|
777
|
+
totalCount?: number; // Required when manualPagination=true
|
|
778
|
+
onStateChange?: (state: DataTableState) => void; // Fires on any state change
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
## Styling & Customization
|
|
783
|
+
|
|
784
|
+
The DataTable uses Tailwind CSS classes and shadcn/ui components. Customize by:
|
|
785
|
+
|
|
786
|
+
1. **Modifying component styles** - Edit the component files directly
|
|
787
|
+
2. **Using Tailwind config** - Customize via `tailwind.config.ts`
|
|
788
|
+
3. **Custom cell rendering** - Use the `cell` prop in column config
|
|
789
|
+
4. **Custom header rendering** - Use `header` prop with JSX
|
|
790
|
+
|
|
791
|
+
## Performance Considerations
|
|
792
|
+
|
|
793
|
+
- **Virtual scrolling**: Consider for tables with 1000+ rows
|
|
794
|
+
- **Pagination**: Use traditional or infinite scroll based on UX needs
|
|
795
|
+
- **Client vs Server filtering**: Choose based on dataset size
|
|
796
|
+
- **Memoization**: Use `React.memo` for expensive cell renders
|
|
797
|
+
- **Key prop**: Always use a stable unique `rowKey`
|
|
798
|
+
|
|
799
|
+
## Accessibility
|
|
800
|
+
|
|
801
|
+
- Fully keyboard navigable (arrow keys, tab, enter)
|
|
802
|
+
- ARIA labels on all interactive elements
|
|
803
|
+
- Semantic HTML structure
|
|
804
|
+
- Screen reader friendly
|
|
805
|
+
- High contrast support
|
|
806
|
+
|
|
807
|
+
## Browser Support
|
|
808
|
+
|
|
809
|
+
- Chrome/Edge (latest 2 versions)
|
|
810
|
+
- Firefox (latest 2 versions)
|
|
811
|
+
- Safari (latest 2 versions)
|
|
812
|
+
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
813
|
+
|
|
814
|
+
## Demo
|
|
815
|
+
|
|
816
|
+
Visit `/data-table-demo` to see all features in action!
|