@rowakit/table 0.1.0 → 0.2.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 +234 -1
- package/dist/index.cjs +289 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -6
- package/dist/index.d.ts +144 -6
- package/dist/index.js +289 -33
- package/dist/index.js.map +1 -1
- package/package.json +7 -8
- package/src/styles/table.css +104 -0
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@ RowaKit Table is a React table component designed for real-world internal applic
|
|
|
11
11
|
✅ **Minimal API** - Few props, convention over configuration
|
|
12
12
|
✅ **Escape hatch** - `col.custom()` for any rendering need
|
|
13
13
|
✅ **Action buttons** - Built-in support for row actions with confirmation
|
|
14
|
-
✅ **
|
|
14
|
+
✅ **7 column types** - Text, Date, Boolean, Badge, Number, Actions, Custom
|
|
15
|
+
✅ **Column modifiers** - Width, align, truncate support (v0.2.0+)
|
|
16
|
+
✅ **Server-side filters** - Type-specific filter UI with auto-generated inputs (v0.2.0+)
|
|
15
17
|
✅ **State management** - Automatic loading, error, and empty states
|
|
16
18
|
✅ **Smart fetching** - Retry on error, stale request handling
|
|
17
19
|
|
|
@@ -235,6 +237,86 @@ col.boolean('enabled', { format: (val) => val ? '✓' : '✗' })
|
|
|
235
237
|
|
|
236
238
|
**Default format:** `'Yes'` / `'No'`
|
|
237
239
|
|
|
240
|
+
#### `col.badge(field, options?)` (v0.2.0+)
|
|
241
|
+
|
|
242
|
+
Badge column with visual tone mapping for status/enum values.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
col.badge('status', {
|
|
246
|
+
header: 'Status',
|
|
247
|
+
map: {
|
|
248
|
+
active: { label: 'Active', tone: 'success' },
|
|
249
|
+
pending: { label: 'Pending', tone: 'warning' },
|
|
250
|
+
inactive: { label: 'Inactive', tone: 'neutral' },
|
|
251
|
+
error: { label: 'Error', tone: 'danger' }
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
col.badge('priority', {
|
|
256
|
+
header: 'Priority',
|
|
257
|
+
sortable: true,
|
|
258
|
+
map: {
|
|
259
|
+
high: { label: 'High', tone: 'danger' },
|
|
260
|
+
medium: { label: 'Medium', tone: 'warning' },
|
|
261
|
+
low: { label: 'Low', tone: 'success' }
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Options:**
|
|
267
|
+
- `header?: string` - Custom header label
|
|
268
|
+
- `sortable?: boolean` - Enable sorting
|
|
269
|
+
- `map?: Record<string, { label: string; tone: BadgeTone }>` - Map values to badge labels and visual tones
|
|
270
|
+
- `width?: number` - Column width in pixels
|
|
271
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
272
|
+
- `truncate?: boolean` - Truncate with ellipsis
|
|
273
|
+
|
|
274
|
+
**Badge Tones:**
|
|
275
|
+
- `'neutral'` - Gray (default)
|
|
276
|
+
- `'success'` - Green
|
|
277
|
+
- `'warning'` - Yellow/Orange
|
|
278
|
+
- `'danger'` - Red
|
|
279
|
+
|
|
280
|
+
#### `col.number(field, options?)` (v0.2.0+)
|
|
281
|
+
|
|
282
|
+
Number column with formatting support (currency, percentages, decimals).
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Basic number
|
|
286
|
+
col.number('quantity')
|
|
287
|
+
|
|
288
|
+
// Currency formatting with Intl.NumberFormat
|
|
289
|
+
col.number('price', {
|
|
290
|
+
header: 'Price',
|
|
291
|
+
sortable: true,
|
|
292
|
+
format: { style: 'currency', currency: 'USD' }
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Percentage
|
|
296
|
+
col.number('discount', {
|
|
297
|
+
header: 'Discount',
|
|
298
|
+
format: { style: 'percent', minimumFractionDigits: 1 }
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Custom formatter
|
|
302
|
+
col.number('score', {
|
|
303
|
+
header: 'Score',
|
|
304
|
+
format: (value) => `${value.toFixed(1)} pts`
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Options:**
|
|
309
|
+
- `header?: string` - Custom header label
|
|
310
|
+
- `sortable?: boolean` - Enable sorting
|
|
311
|
+
- `format?: Intl.NumberFormatOptions | ((value: number) => string)` - Formatting
|
|
312
|
+
- `Intl.NumberFormatOptions`: e.g., `{ style: 'currency', currency: 'USD' }`
|
|
313
|
+
- Function: Custom formatter like `(value) => value.toFixed(2)`
|
|
314
|
+
- `width?: number` - Column width in pixels
|
|
315
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment (defaults to 'right')
|
|
316
|
+
- `truncate?: boolean` - Truncate with ellipsis
|
|
317
|
+
|
|
318
|
+
**Default format:** Plain number with right alignment
|
|
319
|
+
|
|
238
320
|
#### `col.actions(actions)`
|
|
239
321
|
|
|
240
322
|
Actions column with buttons for row operations.
|
|
@@ -294,6 +376,41 @@ col.custom('summary', (row) => {
|
|
|
294
376
|
})
|
|
295
377
|
```
|
|
296
378
|
|
|
379
|
+
### Column Modifiers (v0.2.0+)
|
|
380
|
+
|
|
381
|
+
All column types (text, date, boolean, badge, number) support these optional modifiers:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Width: Set fixed column width in pixels
|
|
385
|
+
col.text('name', { width: 200 })
|
|
386
|
+
|
|
387
|
+
// Align: Control text alignment
|
|
388
|
+
col.text('status', { align: 'center' })
|
|
389
|
+
col.number('price', { align: 'right' }) // numbers default to 'right'
|
|
390
|
+
|
|
391
|
+
// Truncate: Enable text truncation with ellipsis
|
|
392
|
+
col.text('description', { truncate: true, width: 300 })
|
|
393
|
+
|
|
394
|
+
// Combine multiple modifiers
|
|
395
|
+
col.badge('status', {
|
|
396
|
+
header: 'Status',
|
|
397
|
+
width: 120,
|
|
398
|
+
align: 'center',
|
|
399
|
+
truncate: true,
|
|
400
|
+
map: { active: 'success', inactive: 'neutral' }
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Modifiers:**
|
|
405
|
+
- `width?: number` - Column width in pixels
|
|
406
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
407
|
+
- `truncate?: boolean` - Truncate long text with ellipsis (requires `width`)
|
|
408
|
+
|
|
409
|
+
**Notes:**
|
|
410
|
+
- Number columns default to `align: 'right'`
|
|
411
|
+
- Other columns default to `align: 'left'`
|
|
412
|
+
- Truncate works best with a fixed `width`
|
|
413
|
+
|
|
297
414
|
### Pagination
|
|
298
415
|
|
|
299
416
|
The table includes built-in pagination controls that appear automatically when data is loaded.
|
|
@@ -411,6 +528,8 @@ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
|
411
528
|
- ✅ Text columns with `sortable: true`
|
|
412
529
|
- ✅ Date columns with `sortable: true`
|
|
413
530
|
- ✅ Boolean columns with `sortable: true`
|
|
531
|
+
- ✅ Badge columns with `sortable: true` (v0.2.0+)
|
|
532
|
+
- ✅ Number columns with `sortable: true` (v0.2.0+)
|
|
414
533
|
- ❌ Actions columns (never sortable)
|
|
415
534
|
- ❌ Custom columns (not sortable by default)
|
|
416
535
|
|
|
@@ -421,6 +540,120 @@ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
|
421
540
|
- Cleared when clicking a sorted column three times
|
|
422
541
|
- Replaced when clicking a different sortable column
|
|
423
542
|
|
|
543
|
+
### Filters (v0.2.0+)
|
|
544
|
+
|
|
545
|
+
Server-side filtering with type-specific filter inputs rendered in a header row.
|
|
546
|
+
|
|
547
|
+
**Features:**
|
|
548
|
+
- **Auto-Generated UI** - Filter inputs based on column type
|
|
549
|
+
- **Server-Side Only** - All filtering happens in your backend
|
|
550
|
+
- **Multiple Operators** - contains, equals, in, range
|
|
551
|
+
- **Clear Filters** - Individual and bulk filter clearing
|
|
552
|
+
- **Page Reset** - Resets to page 1 when filters change
|
|
553
|
+
|
|
554
|
+
**Enable Filters:**
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
<RowaKitTable
|
|
558
|
+
fetcher={fetchUsers}
|
|
559
|
+
columns={columns}
|
|
560
|
+
rowKey="id"
|
|
561
|
+
enableFilters={true} // Add this prop
|
|
562
|
+
/>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Filter Types by Column:**
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Text column: Text input with "contains" operator
|
|
569
|
+
col.text('name', { header: 'Name' })
|
|
570
|
+
// → User types "john" → filters: { name: { op: 'contains', value: 'john' } }
|
|
571
|
+
|
|
572
|
+
// Number column: Text input with "equals" operator
|
|
573
|
+
col.number('age', { header: 'Age' })
|
|
574
|
+
// → User types "25" → filters: { age: { op: 'equals', value: '25' } }
|
|
575
|
+
|
|
576
|
+
// Badge column: Select dropdown with "equals" operator
|
|
577
|
+
col.badge('status', {
|
|
578
|
+
header: 'Status',
|
|
579
|
+
map: { active: 'success', inactive: 'neutral', pending: 'warning' }
|
|
580
|
+
})
|
|
581
|
+
// → User selects "active" → filters: { status: { op: 'equals', value: 'active' } }
|
|
582
|
+
|
|
583
|
+
// Boolean column: Select dropdown (All/True/False) with "equals" operator
|
|
584
|
+
col.boolean('isVerified', { header: 'Verified' })
|
|
585
|
+
// → User selects "True" → filters: { isVerified: { op: 'equals', value: true } }
|
|
586
|
+
|
|
587
|
+
// Date column: Two date inputs with "range" operator
|
|
588
|
+
col.date('createdAt', { header: 'Created' })
|
|
589
|
+
// → User enters from/to dates → filters: { createdAt: { op: 'range', value: { from: '2024-01-01', to: '2024-12-31' } } }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Fetcher Integration:**
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort, filters }) => {
|
|
596
|
+
// filters is undefined when no filters are active
|
|
597
|
+
// filters = { fieldName: FilterValue, ... } when filtering
|
|
598
|
+
|
|
599
|
+
const params = new URLSearchParams({
|
|
600
|
+
page: String(page),
|
|
601
|
+
limit: String(pageSize),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (sort) {
|
|
605
|
+
params.append('sortBy', sort.field);
|
|
606
|
+
params.append('sortOrder', sort.direction);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (filters) {
|
|
610
|
+
// Example: Convert filters to query params
|
|
611
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
612
|
+
if (filter.op === 'contains') {
|
|
613
|
+
params.append(`${field}_contains`, filter.value);
|
|
614
|
+
} else if (filter.op === 'equals') {
|
|
615
|
+
params.append(field, String(filter.value));
|
|
616
|
+
} else if (filter.op === 'range') {
|
|
617
|
+
if (filter.value.from) params.append(`${field}_from`, filter.value.from);
|
|
618
|
+
if (filter.value.to) params.append(`${field}_to`, filter.value.to);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const response = await fetch(`/api/users?${params}`);
|
|
624
|
+
return response.json();
|
|
625
|
+
};
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Filter Value Types:**
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
type FilterValue =
|
|
632
|
+
| { op: 'contains'; value: string } // Text search
|
|
633
|
+
| { op: 'equals'; value: string | number | boolean | null } // Exact match
|
|
634
|
+
| { op: 'in'; value: Array<string | number> } // Multiple values (future)
|
|
635
|
+
| { op: 'range'; value: { from?: string; to?: string } }; // Date range
|
|
636
|
+
|
|
637
|
+
type Filters = Record<string, FilterValue>;
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Important Rules:**
|
|
641
|
+
|
|
642
|
+
1. **Undefined when empty**: `query.filters` is `undefined` when no filters are active (not `{}`)
|
|
643
|
+
2. **Page resets**: Changing any filter resets page to 1
|
|
644
|
+
3. **No client filtering**: All filtering must be handled by your backend
|
|
645
|
+
4. **Actions/Custom columns**: Not filterable (no filter input rendered)
|
|
646
|
+
|
|
647
|
+
**Clear Filters:**
|
|
648
|
+
|
|
649
|
+
A "Clear all filters" button appears automatically when filters are active:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// User applies filters
|
|
653
|
+
// → "Clear all filters" button appears above table
|
|
654
|
+
// → Click button → all filters cleared → page resets to 1
|
|
655
|
+
```
|
|
656
|
+
|
|
424
657
|
### Actions
|
|
425
658
|
|
|
426
659
|
The table provides built-in support for row actions with confirmation dialogs, loading states, and conditional disabling.
|
package/dist/index.cjs
CHANGED
|
@@ -11,7 +11,10 @@ function text(field, options) {
|
|
|
11
11
|
field,
|
|
12
12
|
header: options?.header,
|
|
13
13
|
sortable: options?.sortable ?? false,
|
|
14
|
-
format: options?.format
|
|
14
|
+
format: options?.format,
|
|
15
|
+
width: options?.width,
|
|
16
|
+
align: options?.align,
|
|
17
|
+
truncate: options?.truncate
|
|
15
18
|
};
|
|
16
19
|
}
|
|
17
20
|
function date(field, options) {
|
|
@@ -21,7 +24,10 @@ function date(field, options) {
|
|
|
21
24
|
field,
|
|
22
25
|
header: options?.header,
|
|
23
26
|
sortable: options?.sortable ?? false,
|
|
24
|
-
format: options?.format
|
|
27
|
+
format: options?.format,
|
|
28
|
+
width: options?.width,
|
|
29
|
+
align: options?.align,
|
|
30
|
+
truncate: options?.truncate
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
function boolean(field, options) {
|
|
@@ -31,7 +37,36 @@ function boolean(field, options) {
|
|
|
31
37
|
field,
|
|
32
38
|
header: options?.header,
|
|
33
39
|
sortable: options?.sortable ?? false,
|
|
34
|
-
format: options?.format
|
|
40
|
+
format: options?.format,
|
|
41
|
+
width: options?.width,
|
|
42
|
+
align: options?.align,
|
|
43
|
+
truncate: options?.truncate
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function badge(field, options) {
|
|
47
|
+
return {
|
|
48
|
+
id: field,
|
|
49
|
+
kind: "badge",
|
|
50
|
+
field,
|
|
51
|
+
header: options?.header,
|
|
52
|
+
sortable: options?.sortable ?? false,
|
|
53
|
+
map: options?.map,
|
|
54
|
+
width: options?.width,
|
|
55
|
+
align: options?.align,
|
|
56
|
+
truncate: options?.truncate
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function number(field, options) {
|
|
60
|
+
return {
|
|
61
|
+
id: field,
|
|
62
|
+
kind: "number",
|
|
63
|
+
field,
|
|
64
|
+
header: options?.header,
|
|
65
|
+
sortable: options?.sortable ?? false,
|
|
66
|
+
format: options?.format,
|
|
67
|
+
width: options?.width,
|
|
68
|
+
align: options?.align,
|
|
69
|
+
truncate: options?.truncate
|
|
35
70
|
};
|
|
36
71
|
}
|
|
37
72
|
function actions(actions2) {
|
|
@@ -41,19 +76,32 @@ function actions(actions2) {
|
|
|
41
76
|
actions: actions2
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
|
-
function custom(
|
|
79
|
+
function custom(arg1, arg2) {
|
|
80
|
+
if (typeof arg1 === "string") {
|
|
81
|
+
if (typeof arg2 !== "function") {
|
|
82
|
+
throw new Error("col.custom(field, render): render must be a function");
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: arg1,
|
|
86
|
+
kind: "custom",
|
|
87
|
+
field: arg1,
|
|
88
|
+
render: arg2
|
|
89
|
+
};
|
|
90
|
+
}
|
|
45
91
|
return {
|
|
46
|
-
id:
|
|
92
|
+
id: arg1.id,
|
|
47
93
|
kind: "custom",
|
|
48
|
-
header:
|
|
49
|
-
field:
|
|
50
|
-
render:
|
|
94
|
+
header: arg1.header,
|
|
95
|
+
field: arg1.field,
|
|
96
|
+
render: arg1.render
|
|
51
97
|
};
|
|
52
98
|
}
|
|
53
99
|
var col = {
|
|
54
100
|
text,
|
|
55
101
|
date,
|
|
56
102
|
boolean,
|
|
103
|
+
badge,
|
|
104
|
+
number,
|
|
57
105
|
actions,
|
|
58
106
|
custom
|
|
59
107
|
};
|
|
@@ -101,6 +149,25 @@ function renderCell(column, row, isLoading, setConfirmState) {
|
|
|
101
149
|
}
|
|
102
150
|
return value ? "Yes" : "No";
|
|
103
151
|
}
|
|
152
|
+
case "badge": {
|
|
153
|
+
const value = row[column.field];
|
|
154
|
+
const valueStr = String(value ?? "");
|
|
155
|
+
const mapped = column.map?.[valueStr];
|
|
156
|
+
const label = mapped?.label ?? valueStr;
|
|
157
|
+
const tone = mapped?.tone ?? "neutral";
|
|
158
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rowakit-badge rowakit-badge-${tone}`, children: label });
|
|
159
|
+
}
|
|
160
|
+
case "number": {
|
|
161
|
+
const value = row[column.field];
|
|
162
|
+
const numValue = Number(value ?? 0);
|
|
163
|
+
if (column.format) {
|
|
164
|
+
if (typeof column.format === "function") {
|
|
165
|
+
return column.format(numValue, row);
|
|
166
|
+
}
|
|
167
|
+
return new Intl.NumberFormat(void 0, column.format).format(numValue);
|
|
168
|
+
}
|
|
169
|
+
return numValue.toLocaleString();
|
|
170
|
+
}
|
|
104
171
|
case "actions": {
|
|
105
172
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: column.actions.map((action) => {
|
|
106
173
|
const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
|
|
@@ -145,7 +212,8 @@ function RowaKitTable({
|
|
|
145
212
|
defaultPageSize = 20,
|
|
146
213
|
pageSizeOptions = [10, 20, 50],
|
|
147
214
|
rowKey,
|
|
148
|
-
className = ""
|
|
215
|
+
className = "",
|
|
216
|
+
enableFilters = false
|
|
149
217
|
}) {
|
|
150
218
|
const [dataState, setDataState] = react.useState({
|
|
151
219
|
state: "idle",
|
|
@@ -156,8 +224,27 @@ function RowaKitTable({
|
|
|
156
224
|
page: 1,
|
|
157
225
|
pageSize: defaultPageSize
|
|
158
226
|
});
|
|
227
|
+
const [filters, setFilters] = react.useState({});
|
|
159
228
|
const [confirmState, setConfirmState] = react.useState(null);
|
|
160
229
|
const requestIdRef = react.useRef(0);
|
|
230
|
+
react.useEffect(() => {
|
|
231
|
+
if (!enableFilters) return;
|
|
232
|
+
const activeFilters = {};
|
|
233
|
+
let hasFilters = false;
|
|
234
|
+
for (const [field, value] of Object.entries(filters)) {
|
|
235
|
+
if (value !== void 0) {
|
|
236
|
+
activeFilters[field] = value;
|
|
237
|
+
hasFilters = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const filtersToSend = hasFilters ? activeFilters : void 0;
|
|
241
|
+
setQuery((prev) => ({
|
|
242
|
+
...prev,
|
|
243
|
+
filters: filtersToSend,
|
|
244
|
+
page: 1
|
|
245
|
+
// Reset page to 1 when filters change
|
|
246
|
+
}));
|
|
247
|
+
}, [filters, enableFilters]);
|
|
161
248
|
react.useEffect(() => {
|
|
162
249
|
const currentRequestId = ++requestIdRef.current;
|
|
163
250
|
setDataState((prev) => ({ ...prev, state: "loading" }));
|
|
@@ -225,38 +312,190 @@ function RowaKitTable({
|
|
|
225
312
|
}
|
|
226
313
|
return query.sort.direction === "asc" ? " \u2191" : " \u2193";
|
|
227
314
|
};
|
|
315
|
+
const handleFilterChange = (field, value) => {
|
|
316
|
+
setFilters((prev) => ({
|
|
317
|
+
...prev,
|
|
318
|
+
[field]: value
|
|
319
|
+
}));
|
|
320
|
+
};
|
|
321
|
+
const handleClearFilter = (field) => {
|
|
322
|
+
setFilters((prev) => {
|
|
323
|
+
const newFilters = { ...prev };
|
|
324
|
+
delete newFilters[field];
|
|
325
|
+
return newFilters;
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
const handleClearAllFilters = () => {
|
|
329
|
+
setFilters({});
|
|
330
|
+
};
|
|
228
331
|
const isLoading = dataState.state === "loading";
|
|
229
332
|
const isError = dataState.state === "error";
|
|
230
333
|
const isEmpty = dataState.state === "empty";
|
|
231
334
|
const totalPages = Math.ceil(dataState.total / query.pageSize);
|
|
232
335
|
const canGoPrevious = query.page > 1 && !isLoading;
|
|
233
336
|
const canGoNext = query.page < totalPages && !isLoading;
|
|
337
|
+
const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
|
|
234
338
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
|
|
339
|
+
hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
340
|
+
"button",
|
|
341
|
+
{
|
|
342
|
+
onClick: handleClearAllFilters,
|
|
343
|
+
className: "rowakit-button rowakit-button-secondary",
|
|
344
|
+
type: "button",
|
|
345
|
+
children: "Clear all filters"
|
|
346
|
+
}
|
|
347
|
+
) }),
|
|
235
348
|
/* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
|
|
236
|
-
/* @__PURE__ */ jsxRuntime.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
e.
|
|
248
|
-
|
|
349
|
+
/* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
|
|
351
|
+
const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
|
|
352
|
+
const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
|
|
353
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
354
|
+
"th",
|
|
355
|
+
{
|
|
356
|
+
onClick: isSortable ? () => handleSort(String(field)) : void 0,
|
|
357
|
+
role: isSortable ? "button" : void 0,
|
|
358
|
+
tabIndex: isSortable ? 0 : void 0,
|
|
359
|
+
onKeyDown: isSortable ? (e) => {
|
|
360
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
handleSort(String(field));
|
|
363
|
+
}
|
|
364
|
+
} : void 0,
|
|
365
|
+
"aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
366
|
+
style: {
|
|
367
|
+
width: column.width ? `${column.width}px` : void 0,
|
|
368
|
+
textAlign: column.align
|
|
369
|
+
},
|
|
370
|
+
className: column.truncate ? "rowakit-cell-truncate" : void 0,
|
|
371
|
+
children: [
|
|
372
|
+
getHeaderLabel(column),
|
|
373
|
+
isSortable && getSortIndicator(String(field))
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
column.id
|
|
377
|
+
);
|
|
378
|
+
}) }),
|
|
379
|
+
enableFilters && /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "rowakit-table-filter-row", children: columns.map((column) => {
|
|
380
|
+
const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
|
|
381
|
+
const canFilter = field && column.kind !== "actions";
|
|
382
|
+
if (!canFilter) {
|
|
383
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", {}, column.id);
|
|
384
|
+
}
|
|
385
|
+
const filterValue = filters[field];
|
|
386
|
+
if (column.kind === "badge") {
|
|
387
|
+
const options = column.map ? Object.keys(column.map) : [];
|
|
388
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
389
|
+
"select",
|
|
390
|
+
{
|
|
391
|
+
className: "rowakit-filter-select",
|
|
392
|
+
value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
|
|
393
|
+
onChange: (e) => {
|
|
394
|
+
const value = e.target.value;
|
|
395
|
+
if (value === "") {
|
|
396
|
+
handleClearFilter(field);
|
|
397
|
+
} else {
|
|
398
|
+
handleFilterChange(field, { op: "equals", value });
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
children: [
|
|
402
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
|
|
403
|
+
options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt, children: opt }, opt))
|
|
404
|
+
]
|
|
249
405
|
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
406
|
+
) }, column.id);
|
|
407
|
+
}
|
|
408
|
+
if (column.kind === "boolean") {
|
|
409
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
410
|
+
"select",
|
|
411
|
+
{
|
|
412
|
+
className: "rowakit-filter-select",
|
|
413
|
+
value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
|
|
414
|
+
onChange: (e) => {
|
|
415
|
+
const value = e.target.value;
|
|
416
|
+
if (value === "") {
|
|
417
|
+
handleClearFilter(field);
|
|
418
|
+
} else {
|
|
419
|
+
handleFilterChange(field, { op: "equals", value: value === "true" });
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
children: [
|
|
423
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
|
|
424
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "True" }),
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "False" })
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
) }, column.id);
|
|
429
|
+
}
|
|
430
|
+
if (column.kind === "date") {
|
|
431
|
+
const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
|
|
432
|
+
const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
|
|
433
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-date-range", children: [
|
|
434
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
435
|
+
"input",
|
|
436
|
+
{
|
|
437
|
+
type: "date",
|
|
438
|
+
className: "rowakit-filter-input",
|
|
439
|
+
placeholder: "From",
|
|
440
|
+
value: fromValue,
|
|
441
|
+
onChange: (e) => {
|
|
442
|
+
const from = e.target.value || void 0;
|
|
443
|
+
const to = toValue || void 0;
|
|
444
|
+
if (!from && !to) {
|
|
445
|
+
handleClearFilter(field);
|
|
446
|
+
} else {
|
|
447
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
453
|
+
"input",
|
|
454
|
+
{
|
|
455
|
+
type: "date",
|
|
456
|
+
className: "rowakit-filter-input",
|
|
457
|
+
placeholder: "To",
|
|
458
|
+
value: toValue,
|
|
459
|
+
onChange: (e) => {
|
|
460
|
+
const to = e.target.value || void 0;
|
|
461
|
+
const from = fromValue || void 0;
|
|
462
|
+
if (!from && !to) {
|
|
463
|
+
handleClearFilter(field);
|
|
464
|
+
} else {
|
|
465
|
+
handleFilterChange(field, { op: "range", value: { from, to } });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
] }) }, column.id);
|
|
471
|
+
}
|
|
472
|
+
const isNumberColumn = column.kind === "number";
|
|
473
|
+
return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
474
|
+
"input",
|
|
475
|
+
{
|
|
476
|
+
type: isNumberColumn ? "number" : "text",
|
|
477
|
+
className: "rowakit-filter-input",
|
|
478
|
+
placeholder: `Filter ${getHeaderLabel(column)}...`,
|
|
479
|
+
value: filterValue?.op === "contains" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "string" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "",
|
|
480
|
+
onChange: (e) => {
|
|
481
|
+
const rawValue = e.target.value;
|
|
482
|
+
if (rawValue === "") {
|
|
483
|
+
handleClearFilter(field);
|
|
484
|
+
} else if (isNumberColumn) {
|
|
485
|
+
const numValue = Number(rawValue);
|
|
486
|
+
if (!isNaN(numValue)) {
|
|
487
|
+
handleFilterChange(field, { op: "equals", value: numValue });
|
|
488
|
+
} else {
|
|
489
|
+
handleClearFilter(field);
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
handleFilterChange(field, { op: "contains", value: rawValue });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
) }, column.id);
|
|
497
|
+
}) })
|
|
498
|
+
] }),
|
|
260
499
|
/* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
|
|
261
500
|
isLoading && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: columns.length, className: "rowakit-table-loading", children: [
|
|
262
501
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-loading-spinner" }),
|
|
@@ -277,7 +516,24 @@ function RowaKitTable({
|
|
|
277
516
|
isEmpty && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: columns.length, className: "rowakit-table-empty", children: "No data" }) }),
|
|
278
517
|
dataState.state === "success" && dataState.items.map((row) => {
|
|
279
518
|
const key = getRowKey(row, rowKey);
|
|
280
|
-
return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) =>
|
|
519
|
+
return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
|
|
520
|
+
const cellClass = [
|
|
521
|
+
column.kind === "number" ? "rowakit-cell-number" : "",
|
|
522
|
+
column.truncate ? "rowakit-cell-truncate" : ""
|
|
523
|
+
].filter(Boolean).join(" ") || void 0;
|
|
524
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
525
|
+
"td",
|
|
526
|
+
{
|
|
527
|
+
className: cellClass,
|
|
528
|
+
style: {
|
|
529
|
+
width: column.width ? `${column.width}px` : void 0,
|
|
530
|
+
textAlign: column.align || (column.kind === "number" ? "right" : void 0)
|
|
531
|
+
},
|
|
532
|
+
children: renderCell(column, row, isLoading, setConfirmState)
|
|
533
|
+
},
|
|
534
|
+
column.id
|
|
535
|
+
);
|
|
536
|
+
}) }, key);
|
|
281
537
|
})
|
|
282
538
|
] })
|
|
283
539
|
] }),
|