vs-datatable 0.6.0 → 1.0.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 +1033 -9
- package/dist/App_OLD.vue.d.ts +2 -0
- package/dist/api/mock/paymentMethods.d.ts +1 -0
- package/dist/api/mock/paymentStatuses.d.ts +1 -0
- package/dist/components/DropDownButton.vue.d.ts +2 -0
- package/dist/components/VsDataTable.vue.d.ts +51 -5
- package/dist/components/VsDataTableFilterDropdown.vue.d.ts +71 -0
- package/dist/components/VsPagination.vue.d.ts +2 -2
- package/dist/components/VsRowsPerPage.vue.d.ts +2 -2
- package/dist/components/VsSearch.vue.d.ts +2 -2
- package/dist/components/layout/VsColumn.vue.d.ts +47 -0
- package/dist/components/layout/VsDFlex.vue.d.ts +36 -0
- package/dist/components/layout/VsRow.vue.d.ts +83 -0
- package/dist/components/ui/VsMultiSelect.vue.d.ts +11 -0
- package/dist/composables/useAsyncOption.d.ts +10 -0
- package/dist/composables/useColumnFilter.d.ts +23 -0
- package/dist/composables/useDataTable.d.ts +12 -2
- package/dist/composables/useDataTablePagination.d.ts +1 -1
- package/dist/composables/useDataTableSort.d.ts +1 -2
- package/dist/composables/useExpandable.d.ts +13 -0
- package/dist/composables/useVsHelper.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/types/datatable.d.ts +88 -16
- package/dist/utils/datatable.d.ts +8 -1
- package/dist/utils/filterFns.d.ts +1 -0
- package/dist/utils/filters.d.ts +11 -0
- package/dist/views/DemoLayout.vue.d.ts +3 -0
- package/dist/vs-datatable.es.js +2289 -410
- package/dist/vs-datatable.umd.js +2 -2
- package/package.json +4 -1
- package/src/styles/base.scss +769 -79
- package/src/styles/base_OLD.scss +1089 -0
- package/src/styles/vs-layout.css +645 -0
package/README.md
CHANGED
|
@@ -4,16 +4,20 @@ A lightweight, feature-rich Vue 3 data table component with sorting, pagination,
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 🔍 **Search &
|
|
8
|
-
- 📊 **Sorting** - Multi-column sorting with
|
|
9
|
-
- 📄 **Pagination** - Server-side and client-side pagination with customizable controls
|
|
7
|
+
- 🔍 **Advanced Search & Filtering** - Built-in search with column-specific filters and operators
|
|
8
|
+
- 📊 **Enhanced Sorting** - Multi-column sorting with priority badges and visual indicators
|
|
9
|
+
- 📄 **Flexible Pagination** - Server-side and client-side pagination with customizable controls
|
|
10
10
|
- ✅ **Row Selection** - Single and multi-row selection with checkbox controls
|
|
11
|
+
- 📁 **Expandable Rows** - Row expansion with accordion mode and loading states
|
|
11
12
|
- 🎨 **Highly Customizable** - Extensive CSS variables, themes, and slot support
|
|
12
|
-
- 📱 **Responsive** - Mobile-friendly
|
|
13
|
-
- 🚀 **Performance** - Optimized for large datasets with server-side support
|
|
14
|
-
- 🎯 **TypeScript** - Full TypeScript support with type definitions
|
|
13
|
+
- 📱 **Responsive Design** - Mobile-friendly with no external dependencies
|
|
14
|
+
- 🚀 **Performance Optimized** - Optimized for large datasets with server-side support
|
|
15
|
+
- 🎯 **TypeScript** - Full TypeScript support with comprehensive type definitions
|
|
15
16
|
- 🎭 **Zero Dependencies** - No Bootstrap, FontAwesome, or other external libraries
|
|
16
17
|
- 🎨 **Theme System** - Built-in themes and easy customization via CSS variables
|
|
18
|
+
- 🔧 **Column Filters** - Multiple filter types: text, multi-select, number-range, date-range, custom
|
|
19
|
+
- 📈 **Async Options** - Support for async filter options with caching
|
|
20
|
+
- 🎪 **Floating UI** - Modern dropdown positioning with @floating-ui/dom
|
|
17
21
|
|
|
18
22
|
## Key Features
|
|
19
23
|
|
|
@@ -40,11 +44,26 @@ A lightweight, feature-rich Vue 3 data table component with sorting, pagination,
|
|
|
40
44
|
- Enhanced slot system
|
|
41
45
|
- Comprehensive documentation
|
|
42
46
|
|
|
43
|
-
### 🔄 **
|
|
47
|
+
### 🔄 **Enhanced Sorting**
|
|
44
48
|
- Client-side and server-side sorting support
|
|
45
|
-
- Multi-column sorting with priority
|
|
49
|
+
- Multi-column sorting with priority badges
|
|
46
50
|
- Visual sort indicators with SVG icons
|
|
47
51
|
- v-model:sort support for reactive sorting
|
|
52
|
+
- Priority-based sorting with numbered badges
|
|
53
|
+
|
|
54
|
+
### 📁 **Expandable Rows**
|
|
55
|
+
- Row expansion with custom content slots
|
|
56
|
+
- Accordion mode for single-row expansion
|
|
57
|
+
- Loading states for async content
|
|
58
|
+
- Programmatic control of expanded rows
|
|
59
|
+
- Custom expand/collapse icons
|
|
60
|
+
|
|
61
|
+
### 🔧 **Advanced Column Filtering**
|
|
62
|
+
- Multiple filter types: text, multi-select, number-range, date-range, custom
|
|
63
|
+
- Rich operators for each filter type
|
|
64
|
+
- Async filter options with caching
|
|
65
|
+
- Custom filter slots for complex scenarios
|
|
66
|
+
- Floating UI positioning with collision detection
|
|
48
67
|
|
|
49
68
|
## Installation
|
|
50
69
|
|
|
@@ -113,7 +132,9 @@ app.use(VsDataTable)
|
|
|
113
132
|
| `rows` | `any[]` | `[]` | Array of data objects |
|
|
114
133
|
| `loading` | `boolean` | `false` | Shows loading state |
|
|
115
134
|
| `showSearch` | `boolean` | `true` | Enable/disable search functionality |
|
|
116
|
-
| `
|
|
135
|
+
| `showHeader` | `boolean` | `true` | Show/hide table header |
|
|
136
|
+
| `showFooter` | `boolean` | `true` | Show/hide table footer |
|
|
137
|
+
| `headerText` | `string` | `''` | Header text for the table |
|
|
117
138
|
| `itemSelected` | `any[] \| null` | `null` | Controlled selection state |
|
|
118
139
|
| `tablename` | `string` | `"default-table"` | Unique identifier for the table |
|
|
119
140
|
| `tableClass` | `string \| string[] \| Record<string, any>` | - | Custom CSS classes for table |
|
|
@@ -132,7 +153,11 @@ app.use(VsDataTable)
|
|
|
132
153
|
| `noDataDescription` | `string` | `'Try adjusting your search criteria'` | Description for no data state |
|
|
133
154
|
| `entriesText` | `string` | `'entries'` | Text for pagination info |
|
|
134
155
|
| `maxVisiblePages` | `number` | `5` | Maximum visible pagination pages |
|
|
156
|
+
| `rowsPerPage` | `number` | `10` | Initial rows per page |
|
|
135
157
|
| `rowKey` | `string \| ((item: any, index: number) => string \| number)` | `'id'` | Key field for row identification |
|
|
158
|
+
| `expandable` | `boolean` | `false` | Enable row expansion functionality |
|
|
159
|
+
| `accordion` | `boolean` | `false` | Accordion mode (only one row expanded at a time) |
|
|
160
|
+
| `expanded` | `(string \| number)[]` | `[]` | Controlled expanded rows state |
|
|
136
161
|
|
|
137
162
|
### Column Definition
|
|
138
163
|
|
|
@@ -143,6 +168,14 @@ interface Column {
|
|
|
143
168
|
width?: string; // Column width percentage
|
|
144
169
|
sortable?: boolean; // Enable sorting
|
|
145
170
|
isKey?: boolean; // Primary key field
|
|
171
|
+
filter?: { // Column filter configuration
|
|
172
|
+
type: 'text' | 'multi-select' | 'number-range' | 'date-range' | 'custom';
|
|
173
|
+
operators?: string[]; // Custom operators for the filter
|
|
174
|
+
asyncOptions?: () => Promise<string[]>; // Async options for multi-select
|
|
175
|
+
filterFn?: (cellValue: any, filterValue: any, row: any) => boolean; // Custom filter function
|
|
176
|
+
filterKey?: string; // Key for custom filter functions
|
|
177
|
+
custom?: string; // Custom filter slot name
|
|
178
|
+
};
|
|
146
179
|
}
|
|
147
180
|
```
|
|
148
181
|
|
|
@@ -174,6 +207,15 @@ interface Sort {
|
|
|
174
207
|
| `update:serverOptions` | `(options: ServerOptions)` | Fired when server options change |
|
|
175
208
|
| `update:serverItemsLength` | `(length: number)` | Fired when total items count changes |
|
|
176
209
|
| `update:sort` | `(sort: Sort[])` | v-model:sort support for reactive sorting |
|
|
210
|
+
| `update:expanded` | `(expanded: (string \| number)[])` | Fired when expanded rows change |
|
|
211
|
+
| `expand-row` | `{ row: any, index: number, rowId: string \| number }` | Fired when a row is expanded |
|
|
212
|
+
| `collapse-row` | `{ row: any, index: number, rowId: string \| number }` | Fired when a row is collapsed |
|
|
213
|
+
| `filter-change` | `Record<string, ColumnFilter>` | Fired when column filters change |
|
|
214
|
+
| `table-mounted` | `()` | Fired when table is mounted |
|
|
215
|
+
| `table-unmounted` | `()` | Fired when table is unmounted |
|
|
216
|
+
| `table-before-mount` | `()` | Fired before table mounts |
|
|
217
|
+
| `data-loaded` | `(data: any[])` | Fired when data is loaded |
|
|
218
|
+
| `data-error` | `(error: any)` | Fired when data loading fails |
|
|
177
219
|
|
|
178
220
|
## Slots
|
|
179
221
|
|
|
@@ -209,6 +251,39 @@ interface Sort {
|
|
|
209
251
|
</template>
|
|
210
252
|
```
|
|
211
253
|
|
|
254
|
+
### Expandable Row Slots
|
|
255
|
+
```vue
|
|
256
|
+
<template #row-expanded="{ item, index }">
|
|
257
|
+
<div class="expanded-content">
|
|
258
|
+
<h4>Details for {{ item.name }}</h4>
|
|
259
|
+
<p>{{ item.description }}</p>
|
|
260
|
+
</div>
|
|
261
|
+
</template>
|
|
262
|
+
|
|
263
|
+
<template #row-expanded-loader="{ item, index }">
|
|
264
|
+
<div class="custom-loader">
|
|
265
|
+
Loading details for {{ item.name }}...
|
|
266
|
+
</div>
|
|
267
|
+
</template>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Custom Filter Slots
|
|
271
|
+
```vue
|
|
272
|
+
<!-- Custom filter slot -->
|
|
273
|
+
<template #StatusFilterSlot="{ filter, apply, clear }">
|
|
274
|
+
<div class="custom-filter">
|
|
275
|
+
<label>Status Filter</label>
|
|
276
|
+
<select v-model="filter.value">
|
|
277
|
+
<option value="">All</option>
|
|
278
|
+
<option value="active">Active</option>
|
|
279
|
+
<option value="inactive">Inactive</option>
|
|
280
|
+
</select>
|
|
281
|
+
<button @click="apply">Apply</button>
|
|
282
|
+
<button @click="clear">Clear</button>
|
|
283
|
+
</div>
|
|
284
|
+
</template>
|
|
285
|
+
```
|
|
286
|
+
|
|
212
287
|
## Advanced Usage
|
|
213
288
|
|
|
214
289
|
### Server-Side Pagination & Sorting
|
|
@@ -381,6 +456,431 @@ The component includes built-in SVG sort icons that automatically show the curre
|
|
|
381
456
|
- **Priority Badge**: Shows sort priority number for multi-column sorting
|
|
382
457
|
- **Hover Effects**: Visual feedback on sortable columns
|
|
383
458
|
|
|
459
|
+
### Enhanced Sorting with Priority Badges
|
|
460
|
+
|
|
461
|
+
VsDataTable features advanced sorting capabilities with visual priority indicators:
|
|
462
|
+
|
|
463
|
+
#### Priority-Based Sorting
|
|
464
|
+
When multiple columns are sorted, each column displays a priority badge showing its sort order:
|
|
465
|
+
|
|
466
|
+
```vue
|
|
467
|
+
<template>
|
|
468
|
+
<VsDataTable
|
|
469
|
+
:columns="columns"
|
|
470
|
+
:rows="data"
|
|
471
|
+
v-model:sort="sortState"
|
|
472
|
+
/>
|
|
473
|
+
</template>
|
|
474
|
+
|
|
475
|
+
<script setup lang="ts">
|
|
476
|
+
const sortState = ref([
|
|
477
|
+
{ field: 'name', order: 'asc', priority: 1 }, // Primary sort
|
|
478
|
+
{ field: 'age', order: 'desc', priority: 2 }, // Secondary sort
|
|
479
|
+
{ field: 'salary', order: 'asc', priority: 3 } // Tertiary sort
|
|
480
|
+
])
|
|
481
|
+
</script>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### Visual Priority Indicators
|
|
485
|
+
- **Priority 1**: Primary sort column (highest priority)
|
|
486
|
+
- **Priority 2**: Secondary sort column
|
|
487
|
+
- **Priority 3+**: Additional sort columns
|
|
488
|
+
- **Badge Display**: Small numbered badges appear next to sort icons
|
|
489
|
+
- **Color Coding**: Different colors for different priority levels
|
|
490
|
+
|
|
491
|
+
#### Interactive Sorting
|
|
492
|
+
Users can click column headers to:
|
|
493
|
+
- **First Click**: Sort ascending with priority 1
|
|
494
|
+
- **Second Click**: Sort descending with priority 1
|
|
495
|
+
- **Third Click**: Remove sort
|
|
496
|
+
- **Shift+Click**: Add as secondary sort with priority 2
|
|
497
|
+
- **Ctrl+Click**: Add as tertiary sort with priority 3
|
|
498
|
+
|
|
499
|
+
#### Sort State Management
|
|
500
|
+
```vue
|
|
501
|
+
<script setup lang="ts">
|
|
502
|
+
// Reactive sort state
|
|
503
|
+
const sortState = ref([])
|
|
504
|
+
|
|
505
|
+
// Programmatic sort control
|
|
506
|
+
const setSort = (field: string, order: 'asc' | 'desc', priority: number = 1) => {
|
|
507
|
+
// Remove existing sort for this field
|
|
508
|
+
sortState.value = sortState.value.filter(s => s.field !== field)
|
|
509
|
+
|
|
510
|
+
// Add new sort with priority
|
|
511
|
+
sortState.value.push({ field, order, priority })
|
|
512
|
+
|
|
513
|
+
// Reorder by priority
|
|
514
|
+
sortState.value.sort((a, b) => a.priority - b.priority)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Clear all sorts
|
|
518
|
+
const clearSorts = () => {
|
|
519
|
+
sortState.value = []
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Clear specific field sort
|
|
523
|
+
const clearFieldSort = (field: string) => {
|
|
524
|
+
sortState.value = sortState.value.filter(s => s.field !== field)
|
|
525
|
+
}
|
|
526
|
+
</script>
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### Server-Side Sorting Integration
|
|
530
|
+
For server-side sorting, the sort state is automatically sent to your server:
|
|
531
|
+
|
|
532
|
+
```vue
|
|
533
|
+
<script setup lang="ts">
|
|
534
|
+
const handleServerOptionsChange = async (options) => {
|
|
535
|
+
const response = await fetch('/api/data', {
|
|
536
|
+
method: 'POST',
|
|
537
|
+
body: JSON.stringify({
|
|
538
|
+
page: options.page,
|
|
539
|
+
limit: options.rowsPerPage,
|
|
540
|
+
sort: options.sort // Array of { field, order, priority }
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
return response.json()
|
|
545
|
+
}
|
|
546
|
+
</script>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### Custom Sort Icons
|
|
550
|
+
You can customize the sort icons by overriding the CSS:
|
|
551
|
+
|
|
552
|
+
```css
|
|
553
|
+
.vs-sort-icon {
|
|
554
|
+
/* Custom sort icon styling */
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.vs-sort-priority {
|
|
558
|
+
/* Custom priority badge styling */
|
|
559
|
+
background: var(--vs-primary);
|
|
560
|
+
color: white;
|
|
561
|
+
border-radius: 50%;
|
|
562
|
+
font-size: 10px;
|
|
563
|
+
font-weight: bold;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Column Filtering
|
|
568
|
+
|
|
569
|
+
VsDataTable provides powerful column-level filtering with multiple filter types and operators.
|
|
570
|
+
|
|
571
|
+
### Filter Types
|
|
572
|
+
|
|
573
|
+
#### Text Filter
|
|
574
|
+
```typescript
|
|
575
|
+
const columns = [
|
|
576
|
+
{
|
|
577
|
+
label: 'Name',
|
|
578
|
+
field: 'name',
|
|
579
|
+
filter: {
|
|
580
|
+
type: 'text',
|
|
581
|
+
operators: ['contains', 'equals', 'startsWith', 'endsWith']
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
**Available Operators:**
|
|
588
|
+
- `contains` - Contains text
|
|
589
|
+
- `doesNotContains` - Does not contain text
|
|
590
|
+
- `equals` - Exact match
|
|
591
|
+
- `doesNotEqual` - Not equal
|
|
592
|
+
- `startsWith` - Starts with text
|
|
593
|
+
- `endsWith` - Ends with text
|
|
594
|
+
- `empty` - Field is empty
|
|
595
|
+
- `notEmpty` - Field is not empty
|
|
596
|
+
|
|
597
|
+
#### Multi-Select Filter
|
|
598
|
+
```typescript
|
|
599
|
+
const columns = [
|
|
600
|
+
{
|
|
601
|
+
label: 'Status',
|
|
602
|
+
field: 'status',
|
|
603
|
+
filter: {
|
|
604
|
+
type: 'multi-select',
|
|
605
|
+
operators: ['in', 'notIn']
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
]
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
#### Number Range Filter
|
|
612
|
+
```typescript
|
|
613
|
+
const columns = [
|
|
614
|
+
{
|
|
615
|
+
label: 'Age',
|
|
616
|
+
field: 'age',
|
|
617
|
+
filter: {
|
|
618
|
+
type: 'number-range',
|
|
619
|
+
operators: ['between', 'equals', 'greaterThan', 'lessThan', 'empty', 'notEmpty']
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
]
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
#### Date Range Filter
|
|
626
|
+
```typescript
|
|
627
|
+
const columns = [
|
|
628
|
+
{
|
|
629
|
+
label: 'Created Date',
|
|
630
|
+
field: 'createdAt',
|
|
631
|
+
filter: {
|
|
632
|
+
type: 'date-range',
|
|
633
|
+
operators: ['between', 'equals', 'before', 'after', 'empty', 'notEmpty']
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### Custom Filter
|
|
640
|
+
```typescript
|
|
641
|
+
const columns = [
|
|
642
|
+
{
|
|
643
|
+
label: 'Custom Field',
|
|
644
|
+
field: 'custom',
|
|
645
|
+
filter: {
|
|
646
|
+
type: 'custom',
|
|
647
|
+
custom: 'CustomFilterSlot'
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
]
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Async Filter Options
|
|
654
|
+
|
|
655
|
+
For multi-select filters, you can load options asynchronously:
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
const columns = [
|
|
659
|
+
{
|
|
660
|
+
label: 'Department',
|
|
661
|
+
field: 'department',
|
|
662
|
+
filter: {
|
|
663
|
+
type: 'multi-select',
|
|
664
|
+
asyncOptions: async () => {
|
|
665
|
+
const response = await fetch('/api/departments')
|
|
666
|
+
return response.json()
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
]
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Complete Filtering Example
|
|
674
|
+
|
|
675
|
+
```vue
|
|
676
|
+
<template>
|
|
677
|
+
<VsDataTable
|
|
678
|
+
:columns="columns"
|
|
679
|
+
:rows="data"
|
|
680
|
+
@filter-change="handleFilterChange"
|
|
681
|
+
>
|
|
682
|
+
<!-- Custom filter slot -->
|
|
683
|
+
<template #CustomFilterSlot="{ filter, apply, clear }">
|
|
684
|
+
<div class="custom-filter">
|
|
685
|
+
<label>Custom Filter</label>
|
|
686
|
+
<input
|
|
687
|
+
type="text"
|
|
688
|
+
v-model="filter.value"
|
|
689
|
+
placeholder="Enter custom value"
|
|
690
|
+
/>
|
|
691
|
+
<button @click="apply">Apply</button>
|
|
692
|
+
<button @click="clear">Clear</button>
|
|
693
|
+
</div>
|
|
694
|
+
</template>
|
|
695
|
+
</VsDataTable>
|
|
696
|
+
</template>
|
|
697
|
+
|
|
698
|
+
<script setup lang="ts">
|
|
699
|
+
const columns = [
|
|
700
|
+
{ label: 'ID', field: 'id', width: '10%' },
|
|
701
|
+
{
|
|
702
|
+
label: 'Name',
|
|
703
|
+
field: 'name',
|
|
704
|
+
filter: {
|
|
705
|
+
type: 'text',
|
|
706
|
+
operators: ['contains', 'equals', 'startsWith']
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
label: 'Age',
|
|
711
|
+
field: 'age',
|
|
712
|
+
filter: {
|
|
713
|
+
type: 'number-range',
|
|
714
|
+
operators: ['between', 'greaterThan', 'lessThan']
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
label: 'Status',
|
|
719
|
+
field: 'status',
|
|
720
|
+
filter: {
|
|
721
|
+
type: 'multi-select',
|
|
722
|
+
asyncOptions: async () => {
|
|
723
|
+
return ['Active', 'Inactive', 'Pending']
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
label: 'Created Date',
|
|
729
|
+
field: 'createdAt',
|
|
730
|
+
filter: {
|
|
731
|
+
type: 'date-range',
|
|
732
|
+
operators: ['between', 'before', 'after']
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
|
|
737
|
+
const handleFilterChange = (filters) => {
|
|
738
|
+
console.log('Active filters:', filters)
|
|
739
|
+
// Handle filter changes for server-side filtering
|
|
740
|
+
}
|
|
741
|
+
</script>
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Expandable Rows
|
|
745
|
+
|
|
746
|
+
VsDataTable supports row expansion functionality with custom content, loading states, and accordion mode.
|
|
747
|
+
|
|
748
|
+
### Basic Expandable Rows
|
|
749
|
+
|
|
750
|
+
```vue
|
|
751
|
+
<template>
|
|
752
|
+
<VsDataTable
|
|
753
|
+
:columns="columns"
|
|
754
|
+
:rows="data"
|
|
755
|
+
expandable
|
|
756
|
+
@expand-row="handleExpand"
|
|
757
|
+
@collapse-row="handleCollapse"
|
|
758
|
+
>
|
|
759
|
+
<template #row-expanded="{ item, index }">
|
|
760
|
+
<div class="expanded-content">
|
|
761
|
+
<h4>Details for {{ item.name }}</h4>
|
|
762
|
+
<p><strong>Description:</strong> {{ item.description }}</p>
|
|
763
|
+
<p><strong>Created:</strong> {{ item.createdAt }}</p>
|
|
764
|
+
</div>
|
|
765
|
+
</template>
|
|
766
|
+
</VsDataTable>
|
|
767
|
+
</template>
|
|
768
|
+
|
|
769
|
+
<script setup lang="ts">
|
|
770
|
+
const handleExpand = ({ row, index, rowId }) => {
|
|
771
|
+
console.log('Row expanded:', row)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const handleCollapse = ({ row, index, rowId }) => {
|
|
775
|
+
console.log('Row collapsed:', row)
|
|
776
|
+
}
|
|
777
|
+
</script>
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Accordion Mode
|
|
781
|
+
|
|
782
|
+
Enable accordion mode to allow only one row to be expanded at a time:
|
|
783
|
+
|
|
784
|
+
```vue
|
|
785
|
+
<template>
|
|
786
|
+
<VsDataTable
|
|
787
|
+
:columns="columns"
|
|
788
|
+
:rows="data"
|
|
789
|
+
expandable
|
|
790
|
+
accordion
|
|
791
|
+
@expand-row="handleExpand"
|
|
792
|
+
>
|
|
793
|
+
<template #row-expanded="{ item, index }">
|
|
794
|
+
<div class="expanded-content">
|
|
795
|
+
<!-- Custom expanded content -->
|
|
796
|
+
</div>
|
|
797
|
+
</template>
|
|
798
|
+
</VsDataTable>
|
|
799
|
+
</template>
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### Controlled Expansion State
|
|
803
|
+
|
|
804
|
+
You can control which rows are expanded using the `expanded` prop:
|
|
805
|
+
|
|
806
|
+
```vue
|
|
807
|
+
<template>
|
|
808
|
+
<VsDataTable
|
|
809
|
+
:columns="columns"
|
|
810
|
+
:rows="data"
|
|
811
|
+
expandable
|
|
812
|
+
v-model:expanded="expandedRows"
|
|
813
|
+
>
|
|
814
|
+
<template #row-expanded="{ item, index }">
|
|
815
|
+
<div class="expanded-content">
|
|
816
|
+
<!-- Custom content -->
|
|
817
|
+
</div>
|
|
818
|
+
</template>
|
|
819
|
+
</VsDataTable>
|
|
820
|
+
</template>
|
|
821
|
+
|
|
822
|
+
<script setup lang="ts">
|
|
823
|
+
const expandedRows = ref<(string | number)[]>(['1', '3'])
|
|
824
|
+
|
|
825
|
+
// Programmatically expand/collapse rows
|
|
826
|
+
const toggleRow = (rowId: string | number) => {
|
|
827
|
+
const index = expandedRows.value.indexOf(rowId)
|
|
828
|
+
if (index > -1) {
|
|
829
|
+
expandedRows.value.splice(index, 1)
|
|
830
|
+
} else {
|
|
831
|
+
expandedRows.value.push(rowId)
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
</script>
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Loading States
|
|
838
|
+
|
|
839
|
+
You can show loading states while fetching expanded content:
|
|
840
|
+
|
|
841
|
+
```vue
|
|
842
|
+
<template>
|
|
843
|
+
<VsDataTable
|
|
844
|
+
ref="tableRef"
|
|
845
|
+
:columns="columns"
|
|
846
|
+
:rows="data"
|
|
847
|
+
expandable
|
|
848
|
+
@expand-row="handleExpand"
|
|
849
|
+
>
|
|
850
|
+
<template #row-expanded="{ item, index }">
|
|
851
|
+
<div class="expanded-content">
|
|
852
|
+
<h4>Details for {{ item.name }}</h4>
|
|
853
|
+
<p>{{ item.details }}</p>
|
|
854
|
+
</div>
|
|
855
|
+
</template>
|
|
856
|
+
|
|
857
|
+
<template #row-expanded-loader="{ item, index }">
|
|
858
|
+
<div class="loading-spinner">
|
|
859
|
+
Loading details for {{ item.name }}...
|
|
860
|
+
</div>
|
|
861
|
+
</template>
|
|
862
|
+
</VsDataTable>
|
|
863
|
+
</template>
|
|
864
|
+
|
|
865
|
+
<script setup lang="ts">
|
|
866
|
+
const tableRef = ref()
|
|
867
|
+
|
|
868
|
+
const handleExpand = async ({ row, index, rowId }) => {
|
|
869
|
+
// Set loading state
|
|
870
|
+
tableRef.value.setRowLoading(rowId, true)
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
// Fetch additional data
|
|
874
|
+
const details = await fetchRowDetails(row.id)
|
|
875
|
+
row.details = details
|
|
876
|
+
} finally {
|
|
877
|
+
// Clear loading state
|
|
878
|
+
tableRef.value.setRowLoading(rowId, false)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
</script>
|
|
882
|
+
```
|
|
883
|
+
|
|
384
884
|
## Styling & Customization
|
|
385
885
|
|
|
386
886
|
### CSS Variables System
|
|
@@ -703,6 +1203,530 @@ onMounted(() => {
|
|
|
703
1203
|
</style>
|
|
704
1204
|
```
|
|
705
1205
|
|
|
1206
|
+
### Advanced Example with All Features
|
|
1207
|
+
|
|
1208
|
+
Here's a comprehensive example showcasing all the new features including column filtering, expandable rows, enhanced sorting, and more:
|
|
1209
|
+
|
|
1210
|
+
```vue
|
|
1211
|
+
<template>
|
|
1212
|
+
<div class="advanced-datatable-demo">
|
|
1213
|
+
<h2>Advanced VsDataTable Demo</h2>
|
|
1214
|
+
|
|
1215
|
+
<VsDataTable
|
|
1216
|
+
ref="tableRef"
|
|
1217
|
+
:columns="columns"
|
|
1218
|
+
:rows="employees"
|
|
1219
|
+
:server-options="serverOptions"
|
|
1220
|
+
:server-items-length="totalEmployees"
|
|
1221
|
+
:loading="loading"
|
|
1222
|
+
v-model:item-selected="selectedEmployees"
|
|
1223
|
+
v-model:sort="sortState"
|
|
1224
|
+
v-model:expanded="expandedRows"
|
|
1225
|
+
expandable
|
|
1226
|
+
accordion
|
|
1227
|
+
header-text="Employee Management System"
|
|
1228
|
+
@update:server-options="fetchEmployees"
|
|
1229
|
+
@sort-changed="handleSortChange"
|
|
1230
|
+
@expand-row="loadEmployeeDetails"
|
|
1231
|
+
@collapse-row="clearEmployeeDetails"
|
|
1232
|
+
@filter-change="handleFilterChange"
|
|
1233
|
+
@row-click="viewEmployee"
|
|
1234
|
+
>
|
|
1235
|
+
<!-- Custom avatar cell -->
|
|
1236
|
+
<template #cell-avatar="{ item }">
|
|
1237
|
+
<img :src="item.avatar" :alt="item.name" class="employee-avatar" />
|
|
1238
|
+
</template>
|
|
1239
|
+
|
|
1240
|
+
<!-- Custom status cell -->
|
|
1241
|
+
<template #cell-status="{ item }">
|
|
1242
|
+
<span :class="`status-badge status-${item.status.toLowerCase()}`">
|
|
1243
|
+
{{ item.status }}
|
|
1244
|
+
</span>
|
|
1245
|
+
</template>
|
|
1246
|
+
|
|
1247
|
+
<!-- Custom salary cell -->
|
|
1248
|
+
<template #cell-salary="{ item }">
|
|
1249
|
+
${{ formatNumber(item.salary) }}
|
|
1250
|
+
</template>
|
|
1251
|
+
|
|
1252
|
+
<!-- Custom actions cell -->
|
|
1253
|
+
<template #cell-actions="{ item }">
|
|
1254
|
+
<div class="action-buttons">
|
|
1255
|
+
<button class="btn-edit" @click.stop="editEmployee(item)">
|
|
1256
|
+
Edit
|
|
1257
|
+
</button>
|
|
1258
|
+
<button class="btn-delete" @click.stop="deleteEmployee(item)">
|
|
1259
|
+
Delete
|
|
1260
|
+
</button>
|
|
1261
|
+
</div>
|
|
1262
|
+
</template>
|
|
1263
|
+
|
|
1264
|
+
<!-- Expanded row content -->
|
|
1265
|
+
<template #row-expanded="{ item, index }">
|
|
1266
|
+
<div class="employee-details">
|
|
1267
|
+
<div class="detail-grid">
|
|
1268
|
+
<div class="detail-section">
|
|
1269
|
+
<h4>Personal Information</h4>
|
|
1270
|
+
<p><strong>Email:</strong> {{ item.email }}</p>
|
|
1271
|
+
<p><strong>Phone:</strong> {{ item.phone }}</p>
|
|
1272
|
+
<p><strong>Birth Date:</strong> {{ formatDate(item.birthDate) }}</p>
|
|
1273
|
+
</div>
|
|
1274
|
+
|
|
1275
|
+
<div class="detail-section">
|
|
1276
|
+
<h4>Work Information</h4>
|
|
1277
|
+
<p><strong>Department:</strong> {{ item.department }}</p>
|
|
1278
|
+
<p><strong>Position:</strong> {{ item.position }}</p>
|
|
1279
|
+
<p><strong>Hire Date:</strong> {{ formatDate(item.hireDate) }}</p>
|
|
1280
|
+
</div>
|
|
1281
|
+
|
|
1282
|
+
<div class="detail-section">
|
|
1283
|
+
<h4>Address</h4>
|
|
1284
|
+
<p>{{ item.address.street }}</p>
|
|
1285
|
+
<p>{{ item.address.city }}, {{ item.address.state }} {{ item.address.zip }}</p>
|
|
1286
|
+
</div>
|
|
1287
|
+
|
|
1288
|
+
<div class="detail-section">
|
|
1289
|
+
<h4>Recent Activity</h4>
|
|
1290
|
+
<ul class="activity-list">
|
|
1291
|
+
<li v-for="activity in item.recentActivity" :key="activity.id">
|
|
1292
|
+
<span class="activity-action">{{ activity.action }}</span>
|
|
1293
|
+
<span class="activity-date">{{ formatDate(activity.date) }}</span>
|
|
1294
|
+
</li>
|
|
1295
|
+
</ul>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
</div>
|
|
1299
|
+
</template>
|
|
1300
|
+
|
|
1301
|
+
<!-- Loading state for expanded rows -->
|
|
1302
|
+
<template #row-expanded-loader="{ item, index }">
|
|
1303
|
+
<div class="loading-details">
|
|
1304
|
+
<div class="spinner"></div>
|
|
1305
|
+
<span>Loading details for {{ item.name }}...</span>
|
|
1306
|
+
</div>
|
|
1307
|
+
</template>
|
|
1308
|
+
|
|
1309
|
+
<!-- Custom filter for department -->
|
|
1310
|
+
<template #DepartmentFilterSlot="{ filter, apply, clear }">
|
|
1311
|
+
<div class="custom-filter">
|
|
1312
|
+
<label>Department Filter</label>
|
|
1313
|
+
<select v-model="filter.value" class="filter-select">
|
|
1314
|
+
<option value="">All Departments</option>
|
|
1315
|
+
<option value="Engineering">Engineering</option>
|
|
1316
|
+
<option value="Marketing">Marketing</option>
|
|
1317
|
+
<option value="Sales">Sales</option>
|
|
1318
|
+
<option value="HR">Human Resources</option>
|
|
1319
|
+
<option value="Finance">Finance</option>
|
|
1320
|
+
</select>
|
|
1321
|
+
<div class="filter-actions">
|
|
1322
|
+
<button class="btn-apply" @click="apply">Apply</button>
|
|
1323
|
+
<button class="btn-clear" @click="clear">Clear</button>
|
|
1324
|
+
</div>
|
|
1325
|
+
</div>
|
|
1326
|
+
</template>
|
|
1327
|
+
</VsDataTable>
|
|
1328
|
+
|
|
1329
|
+
<!-- Bulk actions -->
|
|
1330
|
+
<div v-if="selectedEmployees.length" class="bulk-actions">
|
|
1331
|
+
<h3>Bulk Actions ({{ selectedEmployees.length }} selected)</h3>
|
|
1332
|
+
<button class="btn-bulk-edit" @click="bulkEdit">Edit Selected</button>
|
|
1333
|
+
<button class="btn-bulk-delete" @click="bulkDelete">Delete Selected</button>
|
|
1334
|
+
<button class="btn-bulk-export" @click="exportSelected">Export Selected</button>
|
|
1335
|
+
</div>
|
|
1336
|
+
</div>
|
|
1337
|
+
</template>
|
|
1338
|
+
|
|
1339
|
+
<script setup lang="ts">
|
|
1340
|
+
import { ref, onMounted } from 'vue'
|
|
1341
|
+
import { VsDataTable } from 'vs-datatable'
|
|
1342
|
+
|
|
1343
|
+
const tableRef = ref()
|
|
1344
|
+
const loading = ref(false)
|
|
1345
|
+
const totalEmployees = ref(0)
|
|
1346
|
+
const selectedEmployees = ref([])
|
|
1347
|
+
const expandedRows = ref<(string | number)[]>([])
|
|
1348
|
+
|
|
1349
|
+
const sortState = ref([
|
|
1350
|
+
{ field: 'name', order: 'asc', priority: 1 }
|
|
1351
|
+
])
|
|
1352
|
+
|
|
1353
|
+
const serverOptions = ref({
|
|
1354
|
+
page: 1,
|
|
1355
|
+
rowsPerPage: 10,
|
|
1356
|
+
sort: sortState.value
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
const columns = [
|
|
1360
|
+
{ label: 'Avatar', field: 'avatar', width: '8%' },
|
|
1361
|
+
{
|
|
1362
|
+
label: 'Name',
|
|
1363
|
+
field: 'name',
|
|
1364
|
+
sortable: true,
|
|
1365
|
+
filter: {
|
|
1366
|
+
type: 'text',
|
|
1367
|
+
operators: ['contains', 'equals', 'startsWith']
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
label: 'Department',
|
|
1372
|
+
field: 'department',
|
|
1373
|
+
sortable: true,
|
|
1374
|
+
filter: {
|
|
1375
|
+
type: 'custom',
|
|
1376
|
+
custom: 'DepartmentFilterSlot'
|
|
1377
|
+
}
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
label: 'Position',
|
|
1381
|
+
field: 'position',
|
|
1382
|
+
sortable: true,
|
|
1383
|
+
filter: {
|
|
1384
|
+
type: 'text',
|
|
1385
|
+
operators: ['contains', 'equals']
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
label: 'Status',
|
|
1390
|
+
field: 'status',
|
|
1391
|
+
sortable: true,
|
|
1392
|
+
filter: {
|
|
1393
|
+
type: 'multi-select',
|
|
1394
|
+
asyncOptions: async () => ['Active', 'Inactive', 'On Leave', 'Terminated']
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
label: 'Salary',
|
|
1399
|
+
field: 'salary',
|
|
1400
|
+
sortable: true,
|
|
1401
|
+
filter: {
|
|
1402
|
+
type: 'number-range',
|
|
1403
|
+
operators: ['between', 'greaterThan', 'lessThan']
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
label: 'Hire Date',
|
|
1408
|
+
field: 'hireDate',
|
|
1409
|
+
sortable: true,
|
|
1410
|
+
filter: {
|
|
1411
|
+
type: 'date-range',
|
|
1412
|
+
operators: ['between', 'before', 'after']
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
{ label: 'Actions', field: 'actions', width: '12%' }
|
|
1416
|
+
]
|
|
1417
|
+
|
|
1418
|
+
const employees = ref([
|
|
1419
|
+
{
|
|
1420
|
+
id: 1,
|
|
1421
|
+
name: 'John Smith',
|
|
1422
|
+
email: 'john.smith@company.com',
|
|
1423
|
+
phone: '+1-555-0123',
|
|
1424
|
+
birthDate: '1985-03-15',
|
|
1425
|
+
department: 'Engineering',
|
|
1426
|
+
position: 'Senior Developer',
|
|
1427
|
+
status: 'Active',
|
|
1428
|
+
salary: 95000,
|
|
1429
|
+
hireDate: '2020-01-15',
|
|
1430
|
+
avatar: '/avatars/john.jpg',
|
|
1431
|
+
address: {
|
|
1432
|
+
street: '123 Main St',
|
|
1433
|
+
city: 'San Francisco',
|
|
1434
|
+
state: 'CA',
|
|
1435
|
+
zip: '94102'
|
|
1436
|
+
},
|
|
1437
|
+
recentActivity: []
|
|
1438
|
+
}
|
|
1439
|
+
// ... more employees
|
|
1440
|
+
])
|
|
1441
|
+
|
|
1442
|
+
// Utility functions
|
|
1443
|
+
const formatNumber = (value: number): string => {
|
|
1444
|
+
return new Intl.NumberFormat().format(value)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const formatDate = (date: string): string => {
|
|
1448
|
+
return new Intl.DateTimeFormat('en-US').format(new Date(date))
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Event handlers
|
|
1452
|
+
const fetchEmployees = async (options) => {
|
|
1453
|
+
loading.value = true
|
|
1454
|
+
try {
|
|
1455
|
+
const response = await api.getEmployees({
|
|
1456
|
+
page: options.page,
|
|
1457
|
+
limit: options.rowsPerPage,
|
|
1458
|
+
sort: options.sort,
|
|
1459
|
+
filters: getActiveFilters()
|
|
1460
|
+
})
|
|
1461
|
+
employees.value = response.data
|
|
1462
|
+
totalEmployees.value = response.total
|
|
1463
|
+
serverOptions.value = options
|
|
1464
|
+
} finally {
|
|
1465
|
+
loading.value = false
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const handleSortChange = ({ sort }) => {
|
|
1470
|
+
console.log('Sort changed:', sort)
|
|
1471
|
+
sortState.value = sort
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const loadEmployeeDetails = async ({ row, index, rowId }) => {
|
|
1475
|
+
tableRef.value.setRowLoading(rowId, true)
|
|
1476
|
+
|
|
1477
|
+
try {
|
|
1478
|
+
const activity = await api.getEmployeeActivity(row.id)
|
|
1479
|
+
row.recentActivity = activity
|
|
1480
|
+
} finally {
|
|
1481
|
+
tableRef.value.setRowLoading(rowId, false)
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const clearEmployeeDetails = ({ row, index, rowId }) => {
|
|
1486
|
+
row.recentActivity = []
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const handleFilterChange = (filters) => {
|
|
1490
|
+
console.log('Filters changed:', filters)
|
|
1491
|
+
// Reset to first page when filters change
|
|
1492
|
+
serverOptions.value.page = 1
|
|
1493
|
+
fetchEmployees(serverOptions.value)
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const viewEmployee = (employee) => {
|
|
1497
|
+
console.log('Viewing employee:', employee)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
const editEmployee = (employee) => {
|
|
1501
|
+
console.log('Editing employee:', employee)
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const deleteEmployee = (employee) => {
|
|
1505
|
+
console.log('Deleting employee:', employee)
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const bulkEdit = () => {
|
|
1509
|
+
console.log('Bulk editing:', selectedEmployees.value)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const bulkDelete = () => {
|
|
1513
|
+
console.log('Bulk deleting:', selectedEmployees.value)
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const exportSelected = () => {
|
|
1517
|
+
console.log('Exporting:', selectedEmployees.value)
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const getActiveFilters = () => {
|
|
1521
|
+
// Return current active filters for server request
|
|
1522
|
+
return {}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
onMounted(() => {
|
|
1526
|
+
fetchEmployees(serverOptions.value)
|
|
1527
|
+
})
|
|
1528
|
+
</script>
|
|
1529
|
+
|
|
1530
|
+
<style scoped>
|
|
1531
|
+
.advanced-datatable-demo {
|
|
1532
|
+
padding: 20px;
|
|
1533
|
+
max-width: 1200px;
|
|
1534
|
+
margin: 0 auto;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.employee-avatar {
|
|
1538
|
+
width: 40px;
|
|
1539
|
+
height: 40px;
|
|
1540
|
+
border-radius: 50%;
|
|
1541
|
+
object-fit: cover;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
.status-badge {
|
|
1545
|
+
padding: 4px 8px;
|
|
1546
|
+
border-radius: 4px;
|
|
1547
|
+
font-size: 12px;
|
|
1548
|
+
font-weight: 500;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.status-active {
|
|
1552
|
+
background: #d4edda;
|
|
1553
|
+
color: #155724;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.status-inactive {
|
|
1557
|
+
background: #f8d7da;
|
|
1558
|
+
color: #721c24;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
.status-on-leave {
|
|
1562
|
+
background: #fff3cd;
|
|
1563
|
+
color: #856404;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
.action-buttons {
|
|
1567
|
+
display: flex;
|
|
1568
|
+
gap: 8px;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.btn-edit, .btn-delete {
|
|
1572
|
+
padding: 4px 8px;
|
|
1573
|
+
border: none;
|
|
1574
|
+
border-radius: 4px;
|
|
1575
|
+
cursor: pointer;
|
|
1576
|
+
font-size: 12px;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
.btn-edit {
|
|
1580
|
+
background: #007bff;
|
|
1581
|
+
color: white;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.btn-delete {
|
|
1585
|
+
background: #dc3545;
|
|
1586
|
+
color: white;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.employee-details {
|
|
1590
|
+
padding: 20px;
|
|
1591
|
+
background: #f8f9fa;
|
|
1592
|
+
border-radius: 8px;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.detail-grid {
|
|
1596
|
+
display: grid;
|
|
1597
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
1598
|
+
gap: 20px;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
.detail-section h4 {
|
|
1602
|
+
margin: 0 0 10px 0;
|
|
1603
|
+
color: #333;
|
|
1604
|
+
font-size: 16px;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.activity-list {
|
|
1608
|
+
list-style: none;
|
|
1609
|
+
padding: 0;
|
|
1610
|
+
margin: 0;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
.activity-list li {
|
|
1614
|
+
display: flex;
|
|
1615
|
+
justify-content: space-between;
|
|
1616
|
+
padding: 4px 0;
|
|
1617
|
+
border-bottom: 1px solid #eee;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
.activity-action {
|
|
1621
|
+
font-weight: 500;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
.activity-date {
|
|
1625
|
+
color: #666;
|
|
1626
|
+
font-size: 12px;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.loading-details {
|
|
1630
|
+
display: flex;
|
|
1631
|
+
align-items: center;
|
|
1632
|
+
gap: 10px;
|
|
1633
|
+
padding: 20px;
|
|
1634
|
+
justify-content: center;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.spinner {
|
|
1638
|
+
width: 20px;
|
|
1639
|
+
height: 20px;
|
|
1640
|
+
border: 2px solid #f3f3f3;
|
|
1641
|
+
border-top: 2px solid #007bff;
|
|
1642
|
+
border-radius: 50%;
|
|
1643
|
+
animation: spin 1s linear infinite;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
@keyframes spin {
|
|
1647
|
+
0% { transform: rotate(0deg); }
|
|
1648
|
+
100% { transform: rotate(360deg); }
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
.custom-filter {
|
|
1652
|
+
padding: 16px;
|
|
1653
|
+
min-width: 200px;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
.custom-filter label {
|
|
1657
|
+
display: block;
|
|
1658
|
+
margin-bottom: 8px;
|
|
1659
|
+
font-weight: 500;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
.filter-select {
|
|
1663
|
+
width: 100%;
|
|
1664
|
+
padding: 8px;
|
|
1665
|
+
border: 1px solid #ddd;
|
|
1666
|
+
border-radius: 4px;
|
|
1667
|
+
margin-bottom: 12px;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
.filter-actions {
|
|
1671
|
+
display: flex;
|
|
1672
|
+
gap: 8px;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.btn-apply, .btn-clear {
|
|
1676
|
+
padding: 6px 12px;
|
|
1677
|
+
border: none;
|
|
1678
|
+
border-radius: 4px;
|
|
1679
|
+
cursor: pointer;
|
|
1680
|
+
font-size: 12px;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
.btn-apply {
|
|
1684
|
+
background: #007bff;
|
|
1685
|
+
color: white;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.btn-clear {
|
|
1689
|
+
background: #6c757d;
|
|
1690
|
+
color: white;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
.bulk-actions {
|
|
1694
|
+
margin-top: 20px;
|
|
1695
|
+
padding: 16px;
|
|
1696
|
+
background: #e9ecef;
|
|
1697
|
+
border-radius: 8px;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
.bulk-actions h3 {
|
|
1701
|
+
margin: 0 0 12px 0;
|
|
1702
|
+
font-size: 16px;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
.btn-bulk-edit, .btn-bulk-delete, .btn-bulk-export {
|
|
1706
|
+
padding: 8px 16px;
|
|
1707
|
+
border: none;
|
|
1708
|
+
border-radius: 4px;
|
|
1709
|
+
cursor: pointer;
|
|
1710
|
+
margin-right: 8px;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
.btn-bulk-edit {
|
|
1714
|
+
background: #28a745;
|
|
1715
|
+
color: white;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.btn-bulk-delete {
|
|
1719
|
+
background: #dc3545;
|
|
1720
|
+
color: white;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.btn-bulk-export {
|
|
1724
|
+
background: #17a2b8;
|
|
1725
|
+
color: white;
|
|
1726
|
+
}
|
|
1727
|
+
</style>
|
|
1728
|
+
```
|
|
1729
|
+
|
|
706
1730
|
## Browser Support
|
|
707
1731
|
|
|
708
1732
|
- Chrome 60+
|