@payfit/unity-components 2.55.14 → 2.55.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payfit/unity-components",
3
- "version": "2.55.14",
3
+ "version": "2.55.15",
4
4
  "module": "./dist/esm/index.js",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -93,7 +93,7 @@
93
93
  "tailwind-variants": "3.2.2",
94
94
  "usehooks-ts": "3.1.1",
95
95
  "zod": "4.4.3",
96
- "@payfit/unity-illustrations": "2.55.14"
96
+ "@payfit/unity-illustrations": "2.55.15"
97
97
  },
98
98
  "peerDependencies": {
99
99
  "@hookform/devtools": "^4",
@@ -105,8 +105,8 @@
105
105
  "react-hook-form": "^7",
106
106
  "react-router-dom": "^5",
107
107
  "zod": "^3 || ^4",
108
- "@payfit/unity-icons": "2.55.14",
109
- "@payfit/unity-themes": "2.55.14"
108
+ "@payfit/unity-icons": "2.55.15",
109
+ "@payfit/unity-themes": "2.55.15"
110
110
  },
111
111
  "devDependencies": {
112
112
  "@figma/code-connect": "1.4.8",
@@ -152,14 +152,14 @@
152
152
  "vite": "8.1.4",
153
153
  "vite-plugin-node-polyfills": "0.28.0",
154
154
  "vitest": "4.1.10",
155
- "@payfit/unity-icons": "2.55.14",
156
- "@payfit/unity-illustrations": "2.55.14",
157
- "@payfit/unity-themes": "2.55.14",
158
- "@payfit/vite-configs": "0.0.0-use.local",
159
155
  "@payfit/hr-app-eslint": "0.0.0-use.local",
160
156
  "@payfit/hr-apps-tsconfigs": "0.0.0-use.local",
161
157
  "@payfit/storybook-addon-console-errors": "0.0.0-use.local",
162
- "@payfit/storybook-config": "0.0.0-use.local"
158
+ "@payfit/storybook-config": "0.0.0-use.local",
159
+ "@payfit/unity-icons": "2.55.15",
160
+ "@payfit/unity-illustrations": "2.55.15",
161
+ "@payfit/unity-themes": "2.55.15",
162
+ "@payfit/vite-configs": "0.0.0-use.local"
163
163
  },
164
164
  "peerDependenciesMeta": {
165
165
  "@hookform/devtools": {
@@ -4,9 +4,10 @@ description: >
4
4
  Load when rendering tabular data with Unity. Use it to choose DataTable for
5
5
  managed table state or primitive Table for static/custom markup, including
6
6
  filtering, pagination, virtualization, or bulk actions.
7
- type: core
8
- library: '@payfit/unity-components'
9
- library_version: '2.x'
7
+ metadata:
8
+ type: core
9
+ library: '@payfit/unity-components'
10
+ library_version: '2.x'
10
11
  sources:
11
12
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/DataTable.tsx'
12
13
  - 'PayFit/hr-apps:libs/shared/unity/components/src/components/data-table/parts/DataTableRoot.tsx'
@@ -23,7 +24,9 @@ table state to manage.
23
24
 
24
25
  ## Setup
25
26
 
26
- Minimum working client-side DataTable. Columns and data MUST be memoized.
27
+ Minimum working client-side DataTable. Keep `columns` and `data` references
28
+ stable across renders. Define static columns outside the component or memoize
29
+ them; memoize derived data when its source would otherwise create a new array.
27
30
 
28
31
  ```tsx
29
32
  import { useMemo, useState } from 'react'
@@ -98,241 +101,17 @@ export function EmployeeTable({ employees }: { employees: Employee[] }) {
98
101
 
99
102
  ## Core Patterns
100
103
 
101
- ### Define columns with the column helper and ColumnMeta
104
+ - Define columns with `createColumnHelper` and use `ColumnMeta` for row
105
+ headers, focus behavior, helper text, and fixed-layout widths.
106
+ - For server pagination, pass only the current page, set `manualPagination`,
107
+ and derive `pageCount` from the server total.
108
+ - Map `FilterToolbar.onChange` into TanStack Table's column/global filter
109
+ state.
110
+ - Put `DataTableBulkActions` beside `DataTable` inside `DataTableRoot` and
111
+ drive it from row-selection state.
102
112
 
103
- ColumnMeta drives accessibility (`isRowHeader`), keyboard navigation
104
- (`isFocusable: false` for cells whose children are themselves focusable like
105
- checkboxes), `helperText` (renders a tooltip next to the header), and
106
- `headerClassName` (required when `layout="fixed"`).
107
-
108
- ```tsx
109
- import { Badge } from '@payfit/unity-components'
110
- import { createColumnHelper } from '@tanstack/react-table'
111
-
112
- const columnHelper = createColumnHelper<Employee>()
113
-
114
- export const employeeColumns = [
115
- columnHelper.accessor('name', {
116
- id: 'employee',
117
- header: 'Employee',
118
- enableSorting: true,
119
- meta: { isRowHeader: true, headerClassName: 'uy:w-[260px]' },
120
- }),
121
- columnHelper.accessor('status', {
122
- header: 'Status',
123
- enableColumnFilter: true,
124
- filterFn: 'arrIncludesSome',
125
- cell: info => {
126
- const status = info.getValue()
127
- return (
128
- <Badge variant={status === 'active' ? 'success' : 'neutral'}>
129
- {status}
130
- </Badge>
131
- )
132
- },
133
- meta: { helperText: 'Active employees can sign in.' },
134
- }),
135
- columnHelper.display({
136
- id: 'actions',
137
- header: '',
138
- cell: ({ row }) => <RowMenu row={row.original} />,
139
- meta: { isFocusable: false },
140
- }),
141
- ]
142
- ```
143
-
144
- ### Server-side pagination
145
-
146
- `manualPagination: true` disables Tanstack's internal slicing. Pass only the
147
- current page's slice as `data` and supply `pageCount`.
148
-
149
- ```tsx
150
- import { useMemo, useState } from 'react'
151
-
152
- import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
153
-
154
- export function ServerTable({ totalCount, fetchPage }: Props) {
155
- const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 })
156
- const { data: pageRows = [] } = useQuery({
157
- queryKey: ['employees', pagination],
158
- queryFn: () => fetchPage(pagination.pageIndex, pagination.pageSize),
159
- })
160
-
161
- const columns = useMemo(() => employeeColumns, [])
162
- const data = useMemo(() => pageRows, [pageRows])
163
-
164
- const table = useReactTable({
165
- data,
166
- columns,
167
- manualPagination: true,
168
- pageCount: Math.ceil(totalCount / pagination.pageSize),
169
- state: { pagination },
170
- onPaginationChange: setPagination,
171
- getCoreRowModel: getCoreRowModel(),
172
- })
173
-
174
- return (
175
- <DataTableRoot>
176
- <DataTable table={table}>
177
- {row => (
178
- <TableRow key={row.id}>
179
- {row.getVisibleCells().map(cell => (
180
- <TableCell key={cell.id}>
181
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
182
- </TableCell>
183
- ))}
184
- </TableRow>
185
- )}
186
- </DataTable>
187
- </DataTableRoot>
188
- )
189
- }
190
- ```
191
-
192
- ### Filtering with FilterToolbar
193
-
194
- `FilterToolbar.onChange` emits `SerializableAppliedFilter[]`. Map that onto
195
- `table.setColumnFilters` / `table.setGlobalFilter`. `renderControl` takes any
196
- control — it is a render function on purpose so you can drop in date pickers,
197
- multi-selects, text fields, etc.
198
-
199
- ```tsx
200
- import type { FilterDef } from '@payfit/unity-components'
201
-
202
- import { FilterToolbar, Select, SelectItem } from '@payfit/unity-components'
203
- import { getFilteredRowModel } from '@tanstack/react-table'
204
-
205
- const filterDefs: FilterDef[] = [
206
- {
207
- id: 'status',
208
- label: 'Status',
209
- renderControl: (value, onChange) => (
210
- <Select selectedKey={value as string} onSelectionChange={onChange}>
211
- <SelectItem id="active">Active</SelectItem>
212
- <SelectItem id="inactive">Inactive</SelectItem>
213
- </Select>
214
- ),
215
- renderLabel: value => String(value),
216
- },
217
- ]
218
-
219
- export function FilteredTable() {
220
- const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
221
-
222
- const table = useReactTable({
223
- data,
224
- columns,
225
- state: { columnFilters, pagination },
226
- onColumnFiltersChange: setColumnFilters,
227
- onPaginationChange: setPagination,
228
- getCoreRowModel: getCoreRowModel(),
229
- getFilteredRowModel: getFilteredRowModel(),
230
- getPaginationRowModel: getPaginationRowModel(),
231
- })
232
-
233
- return (
234
- <>
235
- <FilterToolbar
236
- filterDefs={filterDefs}
237
- onChange={applied =>
238
- setColumnFilters(applied.map(f => ({ id: f.id, value: f.value })))
239
- }
240
- />
241
- <DataTableRoot>
242
- <DataTable table={table}>
243
- {row => (
244
- <TableRow key={row.id}>
245
- {row.getVisibleCells().map(cell => (
246
- <TableCell key={cell.id}>
247
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
248
- </TableCell>
249
- ))}
250
- </TableRow>
251
- )}
252
- </DataTable>
253
- </DataTableRoot>
254
- </>
255
- )
256
- }
257
- ```
258
-
259
- ### Bulk actions with row selection
260
-
261
- `DataTableBulkActions` lives next to `DataTable` inside `DataTableRoot`.
262
- It auto-shows when rows are selected and provides the F6 focus trap. Use a
263
- display column with `meta.isFocusable: false` for the checkbox.
264
-
265
- ```tsx
266
- import {
267
- CheckboxStandalone,
268
- DataTable,
269
- DataTableBulkActions,
270
- DataTableRoot,
271
- } from '@payfit/unity-components'
272
-
273
- const checkboxColumn = columnHelper.display({
274
- id: 'select',
275
- header: ({ table }) => (
276
- <CheckboxStandalone
277
- isSelected={table.getIsAllPageRowsSelected()}
278
- isIndeterminate={table.getIsSomePageRowsSelected()}
279
- onChange={value => table.toggleAllPageRowsSelected(value)}
280
- slot="selection"
281
- >
282
- Select all
283
- </CheckboxStandalone>
284
- ),
285
- cell: ({ row }) => (
286
- <CheckboxStandalone
287
- isSelected={row.getIsSelected()}
288
- onChange={value => row.toggleSelected(value)}
289
- slot="selection"
290
- >
291
- Select row
292
- </CheckboxStandalone>
293
- ),
294
- enableSorting: false,
295
- meta: { isFocusable: false },
296
- })
297
-
298
- export function BulkTable() {
299
- const [rowSelection, setRowSelection] = useState({})
300
- const columns = useMemo(() => [checkboxColumn, ...employeeColumns], [])
301
- const table = useReactTable({
302
- data,
303
- columns,
304
- state: { rowSelection, pagination },
305
- onRowSelectionChange: setRowSelection,
306
- onPaginationChange: setPagination,
307
- enableRowSelection: true,
308
- getCoreRowModel: getCoreRowModel(),
309
- getPaginationRowModel: getPaginationRowModel(),
310
- })
311
-
312
- return (
313
- <DataTableRoot>
314
- <DataTable table={table}>
315
- {row => (
316
- <TableRow key={row.id} isSelected={row.getIsSelected()}>
317
- {row.getVisibleCells().map(cell => (
318
- <TableCell key={cell.id}>
319
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
320
- </TableCell>
321
- ))}
322
- </TableRow>
323
- )}
324
- </DataTable>
325
- <DataTableBulkActions
326
- table={table}
327
- actions={[
328
- { id: 'archive', label: 'Archive', onAction: rows => archive(rows) },
329
- { id: 'delete', label: 'Delete', onAction: rows => remove(rows) },
330
- ]}
331
- />
332
- </DataTableRoot>
333
- )
334
- }
335
- ```
113
+ Read [references/patterns.md](references/patterns.md) when implementing one of
114
+ these patterns.
336
115
 
337
116
  ## Common Mistakes
338
117
 
@@ -435,7 +214,7 @@ const table = useReactTable({ /* … */ state: { columnFilters, globalFilter },
435
214
 
436
215
  FilterToolbar.onChange emits AppliedFilter[]. Agent stores them locally without mapping to table.setGlobalFilter / setColumnFilters, so rows do not actually filter.
437
216
 
438
- ### CRITICAL Server-side pagination without slicing data
217
+ ### HIGH Pass every local row with manual pagination
439
218
 
440
219
  Wrong:
441
220
 
@@ -447,7 +226,7 @@ useReactTable({
447
226
  })
448
227
  ```
449
228
 
450
- Correct:
229
+ Correct for locally held data:
451
230
 
452
231
  ```tsx
453
232
  const currentData = useMemo(() => {
@@ -465,6 +244,11 @@ useReactTable({
465
244
 
466
245
  manualPagination: true tells Tanstack not to slice. Agents pass the full dataset and expect TanStack to paginate anyway.
467
246
 
247
+ For server-side pagination, fetch and pass only the current server page and
248
+ set `pageCount` from the server's total count. When all rows are local and
249
+ TanStack should paginate them automatically, omit `manualPagination` and use
250
+ `getPaginationRowModel()` instead.
251
+
468
252
  ### MEDIUM Render large datasets without enableVirtualization
469
253
 
470
254
  Wrong:
@@ -0,0 +1,237 @@
1
+ # Unity DataTable patterns
2
+
3
+ ### Define columns with the column helper and ColumnMeta
4
+
5
+ ColumnMeta drives accessibility (`isRowHeader`), keyboard navigation
6
+ (`isFocusable: false` for cells whose children are themselves focusable like
7
+ checkboxes), `helperText` (renders a tooltip next to the header), and
8
+ `headerClassName` (required when `layout="fixed"`).
9
+
10
+ ```tsx
11
+ import { Badge } from '@payfit/unity-components'
12
+ import { createColumnHelper } from '@tanstack/react-table'
13
+
14
+ const columnHelper = createColumnHelper<Employee>()
15
+
16
+ export const employeeColumns = [
17
+ columnHelper.accessor('name', {
18
+ id: 'employee',
19
+ header: 'Employee',
20
+ enableSorting: true,
21
+ meta: { isRowHeader: true, headerClassName: 'uy:w-[260px]' },
22
+ }),
23
+ columnHelper.accessor('status', {
24
+ header: 'Status',
25
+ enableColumnFilter: true,
26
+ filterFn: 'arrIncludesSome',
27
+ cell: info => {
28
+ const status = info.getValue()
29
+ return (
30
+ <Badge variant={status === 'active' ? 'success' : 'neutral'}>
31
+ {status}
32
+ </Badge>
33
+ )
34
+ },
35
+ meta: { helperText: 'Active employees can sign in.' },
36
+ }),
37
+ columnHelper.display({
38
+ id: 'actions',
39
+ header: '',
40
+ cell: ({ row }) => <RowMenu row={row.original} />,
41
+ meta: { isFocusable: false },
42
+ }),
43
+ ]
44
+ ```
45
+
46
+ ### Server-side pagination
47
+
48
+ `manualPagination: true` disables Tanstack's internal slicing. Pass only the
49
+ current page's slice as `data` and supply `pageCount`.
50
+
51
+ ```tsx
52
+ import { useMemo, useState } from 'react'
53
+
54
+ import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
55
+
56
+ export function ServerTable({ totalCount, fetchPage }: Props) {
57
+ const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 })
58
+ const { data: pageRows = [] } = useQuery({
59
+ queryKey: ['employees', pagination],
60
+ queryFn: () => fetchPage(pagination.pageIndex, pagination.pageSize),
61
+ })
62
+
63
+ const columns = useMemo(() => employeeColumns, [])
64
+ const data = useMemo(() => pageRows, [pageRows])
65
+
66
+ const table = useReactTable({
67
+ data,
68
+ columns,
69
+ manualPagination: true,
70
+ pageCount: Math.ceil(totalCount / pagination.pageSize),
71
+ state: { pagination },
72
+ onPaginationChange: setPagination,
73
+ getCoreRowModel: getCoreRowModel(),
74
+ })
75
+
76
+ return (
77
+ <DataTableRoot>
78
+ <DataTable table={table}>
79
+ {row => (
80
+ <TableRow key={row.id}>
81
+ {row.getVisibleCells().map(cell => (
82
+ <TableCell key={cell.id}>
83
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
84
+ </TableCell>
85
+ ))}
86
+ </TableRow>
87
+ )}
88
+ </DataTable>
89
+ </DataTableRoot>
90
+ )
91
+ }
92
+ ```
93
+
94
+ ### Filtering with FilterToolbar
95
+
96
+ `FilterToolbar.onChange` emits `SerializableAppliedFilter[]`. Map that onto
97
+ `table.setColumnFilters` / `table.setGlobalFilter`. `renderControl` takes any
98
+ control — it is a render function on purpose so you can drop in date pickers,
99
+ multi-selects, text fields, etc.
100
+
101
+ ```tsx
102
+ import type { FilterDef } from '@payfit/unity-components'
103
+
104
+ import { FilterToolbar, Select, SelectItem } from '@payfit/unity-components'
105
+ import { getFilteredRowModel } from '@tanstack/react-table'
106
+
107
+ const filterDefs: FilterDef[] = [
108
+ {
109
+ id: 'status',
110
+ label: 'Status',
111
+ renderControl: (value, onChange) => (
112
+ <Select selectedKey={value as string} onSelectionChange={onChange}>
113
+ <SelectItem id="active">Active</SelectItem>
114
+ <SelectItem id="inactive">Inactive</SelectItem>
115
+ </Select>
116
+ ),
117
+ renderLabel: value => String(value),
118
+ },
119
+ ]
120
+
121
+ export function FilteredTable() {
122
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
123
+
124
+ const table = useReactTable({
125
+ data,
126
+ columns,
127
+ state: { columnFilters, pagination },
128
+ onColumnFiltersChange: setColumnFilters,
129
+ onPaginationChange: setPagination,
130
+ getCoreRowModel: getCoreRowModel(),
131
+ getFilteredRowModel: getFilteredRowModel(),
132
+ getPaginationRowModel: getPaginationRowModel(),
133
+ })
134
+
135
+ return (
136
+ <>
137
+ <FilterToolbar
138
+ filterDefs={filterDefs}
139
+ onChange={applied =>
140
+ setColumnFilters(applied.map(f => ({ id: f.id, value: f.value })))
141
+ }
142
+ />
143
+ <DataTableRoot>
144
+ <DataTable table={table}>
145
+ {row => (
146
+ <TableRow key={row.id}>
147
+ {row.getVisibleCells().map(cell => (
148
+ <TableCell key={cell.id}>
149
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
150
+ </TableCell>
151
+ ))}
152
+ </TableRow>
153
+ )}
154
+ </DataTable>
155
+ </DataTableRoot>
156
+ </>
157
+ )
158
+ }
159
+ ```
160
+
161
+ ### Bulk actions with row selection
162
+
163
+ `DataTableBulkActions` lives next to `DataTable` inside `DataTableRoot`.
164
+ It auto-shows when rows are selected and provides the F6 focus trap. Use a
165
+ display column with `meta.isFocusable: false` for the checkbox.
166
+
167
+ ```tsx
168
+ import {
169
+ CheckboxStandalone,
170
+ DataTable,
171
+ DataTableBulkActions,
172
+ DataTableRoot,
173
+ } from '@payfit/unity-components'
174
+
175
+ const checkboxColumn = columnHelper.display({
176
+ id: 'select',
177
+ header: ({ table }) => (
178
+ <CheckboxStandalone
179
+ isSelected={table.getIsAllPageRowsSelected()}
180
+ isIndeterminate={table.getIsSomePageRowsSelected()}
181
+ onChange={value => table.toggleAllPageRowsSelected(value)}
182
+ slot="selection"
183
+ >
184
+ Select all
185
+ </CheckboxStandalone>
186
+ ),
187
+ cell: ({ row }) => (
188
+ <CheckboxStandalone
189
+ isSelected={row.getIsSelected()}
190
+ onChange={value => row.toggleSelected(value)}
191
+ slot="selection"
192
+ >
193
+ Select row
194
+ </CheckboxStandalone>
195
+ ),
196
+ enableSorting: false,
197
+ meta: { isFocusable: false },
198
+ })
199
+
200
+ export function BulkTable() {
201
+ const [rowSelection, setRowSelection] = useState({})
202
+ const columns = useMemo(() => [checkboxColumn, ...employeeColumns], [])
203
+ const table = useReactTable({
204
+ data,
205
+ columns,
206
+ state: { rowSelection, pagination },
207
+ onRowSelectionChange: setRowSelection,
208
+ onPaginationChange: setPagination,
209
+ enableRowSelection: true,
210
+ getCoreRowModel: getCoreRowModel(),
211
+ getPaginationRowModel: getPaginationRowModel(),
212
+ })
213
+
214
+ return (
215
+ <DataTableRoot>
216
+ <DataTable table={table}>
217
+ {row => (
218
+ <TableRow key={row.id} isSelected={row.getIsSelected()}>
219
+ {row.getVisibleCells().map(cell => (
220
+ <TableCell key={cell.id}>
221
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
222
+ </TableCell>
223
+ ))}
224
+ </TableRow>
225
+ )}
226
+ </DataTable>
227
+ <DataTableBulkActions
228
+ table={table}
229
+ actions={[
230
+ { id: 'archive', label: 'Archive', onAction: rows => archive(rows) },
231
+ { id: 'delete', label: 'Delete', onAction: rows => remove(rows) },
232
+ ]}
233
+ />
234
+ </DataTableRoot>
235
+ )
236
+ }
237
+ ```