@stonecrop/atable 0.6.0 → 0.6.2
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/dist/assets/index.css +1 -1
- package/dist/atable.d.ts +153 -3
- package/dist/atable.js +1367 -1138
- package/dist/atable.js.map +1 -1
- package/dist/atable.umd.cjs +2 -2
- package/dist/atable.umd.cjs.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/stores/table.d.ts +125 -3
- package/dist/src/stores/table.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +26 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/stores/table.js +166 -0
- package/package.json +3 -3
- package/src/components/ATable.vue +10 -6
- package/src/components/ATableColumnFilter.vue +212 -0
- package/src/components/ATableHeader.vue +36 -1
- package/src/index.ts +1 -0
- package/src/stores/table.ts +199 -0
- package/src/types/index.ts +31 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="column-filter">
|
|
3
|
+
<input
|
|
4
|
+
v-if="(column.filterType || 'text') === 'text'"
|
|
5
|
+
v-model="filterValue"
|
|
6
|
+
type="text"
|
|
7
|
+
class="filter-input"
|
|
8
|
+
@input="updateFilter(filterValue)" />
|
|
9
|
+
|
|
10
|
+
<input
|
|
11
|
+
v-else-if="column.filterType === 'number'"
|
|
12
|
+
v-model="filterValue"
|
|
13
|
+
type="number"
|
|
14
|
+
class="filter-input"
|
|
15
|
+
@input="updateFilter(filterValue)" />
|
|
16
|
+
|
|
17
|
+
<label v-else-if="column.filterType === 'checkbox'" class="checkbox-filter">
|
|
18
|
+
<input v-model="filterValue" type="checkbox" class="filter-checkbox" @change="updateFilter(filterValue)" />
|
|
19
|
+
<span>{{ column.label }}</span>
|
|
20
|
+
</label>
|
|
21
|
+
|
|
22
|
+
<select
|
|
23
|
+
v-else-if="column.filterType === 'select'"
|
|
24
|
+
v-model="filterValue"
|
|
25
|
+
class="filter-select"
|
|
26
|
+
@change="updateFilter(filterValue)">
|
|
27
|
+
<option value="">All</option>
|
|
28
|
+
<option v-for="option in getSelectOptions(column)" :key="option.value || option" :value="option.value || option">
|
|
29
|
+
{{ option.label || option }}
|
|
30
|
+
</option>
|
|
31
|
+
</select>
|
|
32
|
+
|
|
33
|
+
<input
|
|
34
|
+
v-else-if="column.filterType === 'date'"
|
|
35
|
+
v-model="filterValue"
|
|
36
|
+
type="date"
|
|
37
|
+
class="filter-input"
|
|
38
|
+
@change="updateFilter(filterValue)" />
|
|
39
|
+
|
|
40
|
+
<div v-else-if="column.filterType === 'dateRange'" class="date-range-filter">
|
|
41
|
+
<input
|
|
42
|
+
v-model="dateFilter.startValue"
|
|
43
|
+
type="date"
|
|
44
|
+
class="filter-input"
|
|
45
|
+
@change="updateDateRangeFilter('start', dateFilter.startValue)" />
|
|
46
|
+
<span class="date-separator">-</span>
|
|
47
|
+
<input
|
|
48
|
+
v-model="dateFilter.endValue"
|
|
49
|
+
type="date"
|
|
50
|
+
class="filter-input"
|
|
51
|
+
@change="updateDateRangeFilter('end', dateFilter.endValue)" />
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<component
|
|
55
|
+
v-else-if="column.filterType === 'component' && column.filterComponent"
|
|
56
|
+
:is="column.filterComponent"
|
|
57
|
+
:value="filterValue"
|
|
58
|
+
:column="column"
|
|
59
|
+
:colIndex="colIndex"
|
|
60
|
+
:store="store"
|
|
61
|
+
@update:value="updateFilter($event)" />
|
|
62
|
+
|
|
63
|
+
<button v-if="hasActiveFilter" @click="clearFilter" class="clear-btn" title="Clear">×</button>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
import { ref, reactive, computed } from 'vue'
|
|
69
|
+
import { createTableStore } from '../stores/table'
|
|
70
|
+
import type { TableColumn } from '../types'
|
|
71
|
+
|
|
72
|
+
const { column, colIndex, store } = defineProps<{
|
|
73
|
+
column: TableColumn
|
|
74
|
+
colIndex: number
|
|
75
|
+
store: ReturnType<typeof createTableStore>
|
|
76
|
+
}>()
|
|
77
|
+
|
|
78
|
+
const filterValue = ref<any>('')
|
|
79
|
+
const dateFilter = reactive({
|
|
80
|
+
startValue: '' as string,
|
|
81
|
+
endValue: '' as string,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const getSelectOptions = (column: TableColumn): any[] => {
|
|
85
|
+
if (column.filterOptions) return column.filterOptions
|
|
86
|
+
|
|
87
|
+
// Auto-generate options from data
|
|
88
|
+
const uniqueValues = new Set<any>()
|
|
89
|
+
store.rows.forEach(row => {
|
|
90
|
+
const value = row[column.name]
|
|
91
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
92
|
+
uniqueValues.add(value)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return Array.from(uniqueValues).map(value => ({
|
|
97
|
+
value: value,
|
|
98
|
+
label: String(value),
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const hasActiveFilter = computed(() => {
|
|
103
|
+
return !!(filterValue.value || dateFilter.startValue || dateFilter.endValue)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Filter actions
|
|
107
|
+
const updateFilter = (value: any) => {
|
|
108
|
+
if (!value && column.filterType !== 'checkbox') {
|
|
109
|
+
store.clearFilter(colIndex)
|
|
110
|
+
filterValue.value = ''
|
|
111
|
+
} else {
|
|
112
|
+
filterValue.value = value
|
|
113
|
+
store.setFilter(colIndex, { value })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const updateDateRangeFilter = (rangeType: 'start' | 'end', value: any) => {
|
|
118
|
+
if (rangeType === 'start') {
|
|
119
|
+
dateFilter.startValue = value
|
|
120
|
+
} else {
|
|
121
|
+
dateFilter.endValue = value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!dateFilter.startValue && !dateFilter.endValue) {
|
|
125
|
+
store.clearFilter(colIndex)
|
|
126
|
+
} else {
|
|
127
|
+
store.setFilter(colIndex, {
|
|
128
|
+
value: null,
|
|
129
|
+
startValue: dateFilter.startValue,
|
|
130
|
+
endValue: dateFilter.endValue,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const clearFilter = () => {
|
|
136
|
+
filterValue.value = ''
|
|
137
|
+
dateFilter.startValue = ''
|
|
138
|
+
dateFilter.endValue = ''
|
|
139
|
+
store.clearFilter(colIndex)
|
|
140
|
+
}
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<style scoped>
|
|
144
|
+
.column-filter {
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
gap: 0.25rem;
|
|
148
|
+
width: 100%;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.filter-input,
|
|
152
|
+
.filter-select {
|
|
153
|
+
background-color: var(--sc-form-background) !important;
|
|
154
|
+
padding: 0.15rem 0.2rem;
|
|
155
|
+
border: 1px solid var(--sc-form-border);
|
|
156
|
+
border-radius: 3px;
|
|
157
|
+
font-size: 0.875rem;
|
|
158
|
+
color: var(--sc-text-color);
|
|
159
|
+
width: 100%;
|
|
160
|
+
box-sizing: border-box;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.filter-input:focus,
|
|
164
|
+
.filter-select:focus {
|
|
165
|
+
outline: none;
|
|
166
|
+
border-color: var(--sc-input-active-border-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.checkbox-filter {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 0.25rem;
|
|
173
|
+
font-size: 0.875rem;
|
|
174
|
+
color: var(--sc-text-color);
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.filter-checkbox {
|
|
179
|
+
margin: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.date-range-filter {
|
|
183
|
+
display: flex;
|
|
184
|
+
gap: 0.25rem;
|
|
185
|
+
align-items: center;
|
|
186
|
+
width: 100%;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.date-range-filter .filter-input {
|
|
190
|
+
flex: 1;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.date-separator {
|
|
195
|
+
color: var(--sc-gray-50);
|
|
196
|
+
font-weight: 500;
|
|
197
|
+
padding: 0 0.25rem;
|
|
198
|
+
flex-shrink: 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.clear-btn {
|
|
202
|
+
background: var(--sc-gray-10, #f0f0f0);
|
|
203
|
+
border: 1px solid var(--sc-form-border);
|
|
204
|
+
border-radius: 3px;
|
|
205
|
+
color: var(--sc-gray-70);
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
font-size: 1rem;
|
|
208
|
+
padding: 0.15rem 0.4rem;
|
|
209
|
+
line-height: 1;
|
|
210
|
+
flex-shrink: 0;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<thead v-if="columns.length">
|
|
3
|
+
<!-- Header row -->
|
|
3
4
|
<tr class="atable-header-row" tabindex="-1">
|
|
4
5
|
<th
|
|
5
6
|
v-if="store.zeroColumn"
|
|
@@ -17,15 +18,36 @@
|
|
|
17
18
|
:data-colindex="colKey"
|
|
18
19
|
tabindex="-1"
|
|
19
20
|
:style="store.getHeaderCellStyle(column)"
|
|
20
|
-
:class="column.pinned ? 'sticky-column' : ''"
|
|
21
|
+
:class="`${column.pinned ? 'sticky-column' : ''} ${column.sortable === false ? '' : 'cursor-pointer'}`"
|
|
22
|
+
@click="column.sortable !== false ? handleSort(colKey) : undefined">
|
|
21
23
|
<slot>{{ column.label || String.fromCharCode(colKey + 97).toUpperCase() }}</slot>
|
|
22
24
|
</th>
|
|
23
25
|
</tr>
|
|
26
|
+
<!-- Filters row -->
|
|
27
|
+
<tr v-if="filterableColumns.length > 0" class="atable-filters-row">
|
|
28
|
+
<th
|
|
29
|
+
v-if="store.zeroColumn"
|
|
30
|
+
:class="[
|
|
31
|
+
store.hasPinnedColumns ? 'sticky-index' : '',
|
|
32
|
+
store.isTreeView ? 'tree-index' : '',
|
|
33
|
+
store.config.view === 'list-expansion' ? 'list-expansion-index' : '',
|
|
34
|
+
]"
|
|
35
|
+
class="list-index" />
|
|
36
|
+
<th
|
|
37
|
+
v-for="(column, colKey) in columns"
|
|
38
|
+
:key="`filter-${column.name}`"
|
|
39
|
+
:class="`${column.pinned ? 'sticky-column' : ''}`"
|
|
40
|
+
:style="store.getHeaderCellStyle(column)">
|
|
41
|
+
<ATableColumnFilter v-if="column.filterable" :column="column" :col-index="colKey" :store="store" />
|
|
42
|
+
</th>
|
|
43
|
+
</tr>
|
|
24
44
|
</thead>
|
|
25
45
|
</template>
|
|
26
46
|
|
|
27
47
|
<script setup lang="ts">
|
|
48
|
+
import { computed } from 'vue'
|
|
28
49
|
import { vResizeObserver } from '@vueuse/components'
|
|
50
|
+
import ATableColumnFilter from './ATableColumnFilter.vue'
|
|
29
51
|
import { createTableStore } from '../stores/table'
|
|
30
52
|
import type { TableColumn } from '../types'
|
|
31
53
|
|
|
@@ -34,6 +56,10 @@ const { columns, store } = defineProps<{
|
|
|
34
56
|
store: ReturnType<typeof createTableStore>
|
|
35
57
|
}>()
|
|
36
58
|
|
|
59
|
+
const filterableColumns = computed(() => columns.filter(column => column.filterable))
|
|
60
|
+
|
|
61
|
+
const handleSort = (colIndex: number) => store.sortByColumn(colIndex)
|
|
62
|
+
|
|
37
63
|
const onResize = (entries: ReadonlyArray<ResizeObserverEntry>) => {
|
|
38
64
|
for (const entry of entries) {
|
|
39
65
|
if (entry.borderBoxSize.length === 0) continue
|
|
@@ -75,4 +101,13 @@ th {
|
|
|
75
101
|
width: 2ch;
|
|
76
102
|
margin-left: 5px;
|
|
77
103
|
}
|
|
104
|
+
|
|
105
|
+
.cursor-pointer {
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.atable-filters-row th {
|
|
110
|
+
padding: 0.25rem 0.5ch;
|
|
111
|
+
vertical-align: top;
|
|
112
|
+
}
|
|
78
113
|
</style>
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import ATableLoading from './components/ATableLoading.vue'
|
|
|
10
10
|
import ATableLoadingBar from './components/ATableLoadingBar.vue'
|
|
11
11
|
import ATableModal from './components/ATableModal.vue'
|
|
12
12
|
export { createTableStore } from './stores/table'
|
|
13
|
+
export type { FilterState, FilterStateRecord } from './stores/table'
|
|
13
14
|
export type * from './types'
|
|
14
15
|
|
|
15
16
|
/**
|
package/src/stores/table.ts
CHANGED
|
@@ -15,6 +15,25 @@ import type {
|
|
|
15
15
|
} from '../types'
|
|
16
16
|
import { generateHash } from '../utils'
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Represents the state of a single filter
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface FilterState {
|
|
23
|
+
/** The main filter value */
|
|
24
|
+
value: any
|
|
25
|
+
/** Start value for date range filters */
|
|
26
|
+
startValue?: any
|
|
27
|
+
/** End value for date range filters */
|
|
28
|
+
endValue?: any
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Record mapping column indices to their filter states
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export type FilterStateRecord = Record<number, FilterState>
|
|
36
|
+
|
|
18
37
|
/**
|
|
19
38
|
* Create a table store
|
|
20
39
|
* @param initData - Initial data for the table store
|
|
@@ -186,6 +205,11 @@ export const createTableStore = (initData: {
|
|
|
186
205
|
const ganttBars = ref<GanttBarInfo[]>([])
|
|
187
206
|
const connectionHandles = ref<ConnectionHandle[]>([])
|
|
188
207
|
const connectionPaths = ref<ConnectionPath[]>([])
|
|
208
|
+
const sortState = ref<{ column: number | null; direction: 'asc' | 'desc' | null }>({
|
|
209
|
+
column: null,
|
|
210
|
+
direction: null,
|
|
211
|
+
})
|
|
212
|
+
const filterState = ref<FilterStateRecord>({})
|
|
189
213
|
|
|
190
214
|
// getters
|
|
191
215
|
const hasPinnedColumns = computed(() => columns.value.some(col => col.pinned))
|
|
@@ -207,6 +231,63 @@ export const createTableStore = (initData: {
|
|
|
207
231
|
config.value.view ? ['list', 'tree', 'tree-gantt', 'list-expansion'].includes(config.value.view) : false
|
|
208
232
|
)
|
|
209
233
|
|
|
234
|
+
const filteredRows = computed(() => {
|
|
235
|
+
let filtered = rows.value.map((row, originalIndex) => ({
|
|
236
|
+
...row,
|
|
237
|
+
originalIndex,
|
|
238
|
+
}))
|
|
239
|
+
|
|
240
|
+
// Apply filters
|
|
241
|
+
Object.entries(filterState.value).forEach(([colIndexStr, filter]) => {
|
|
242
|
+
const colIndex = parseInt(colIndexStr)
|
|
243
|
+
const column = columns.value[colIndex]
|
|
244
|
+
|
|
245
|
+
if (!column) return
|
|
246
|
+
|
|
247
|
+
// Skip if filter has no value (except for dateRange and checkbox which can have different value structures)
|
|
248
|
+
const hasFilterValue =
|
|
249
|
+
filter.value ||
|
|
250
|
+
filter.startValue ||
|
|
251
|
+
filter.endValue ||
|
|
252
|
+
(column.filterType === 'checkbox' && filter.value !== undefined)
|
|
253
|
+
|
|
254
|
+
if (!hasFilterValue) return
|
|
255
|
+
|
|
256
|
+
filtered = filtered.filter(row => {
|
|
257
|
+
const cellValue = row[column.name]
|
|
258
|
+
return applyFilter(cellValue, filter, column)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Apply sorting if active
|
|
263
|
+
if (sortState.value.column !== null && sortState.value.direction) {
|
|
264
|
+
const column = columns.value[sortState.value.column]
|
|
265
|
+
const direction = sortState.value.direction
|
|
266
|
+
|
|
267
|
+
filtered.sort((a, b) => {
|
|
268
|
+
let aVal = a[column.name]
|
|
269
|
+
let bVal = b[column.name]
|
|
270
|
+
|
|
271
|
+
if (aVal === null || aVal === undefined) aVal = ''
|
|
272
|
+
if (bVal === null || bVal === undefined) bVal = ''
|
|
273
|
+
|
|
274
|
+
const aNum = Number(aVal)
|
|
275
|
+
const bNum = Number(bVal)
|
|
276
|
+
const isNumeric = !isNaN(aNum) && !isNaN(bNum) && aVal !== '' && bVal !== ''
|
|
277
|
+
|
|
278
|
+
if (isNumeric) {
|
|
279
|
+
return direction === 'asc' ? aNum - bNum : bNum - aNum
|
|
280
|
+
} else {
|
|
281
|
+
const aStr = String(aVal).toLowerCase()
|
|
282
|
+
const bStr = String(bVal).toLowerCase()
|
|
283
|
+
return direction === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return filtered
|
|
289
|
+
})
|
|
290
|
+
|
|
210
291
|
// actions
|
|
211
292
|
const getCellData = <T = any>(colIndex: number, rowIndex: number): T => table.value[`${colIndex}:${rowIndex}`]
|
|
212
293
|
const setCellData = (colIndex: number, rowIndex: number, value: any) => {
|
|
@@ -484,6 +565,118 @@ export const createTableStore = (initData: {
|
|
|
484
565
|
return connectionHandles.value.filter(handle => handle.barId === barId)
|
|
485
566
|
}
|
|
486
567
|
|
|
568
|
+
const sortByColumn = (colIndex: number) => {
|
|
569
|
+
const column = columns.value[colIndex]
|
|
570
|
+
if (column.sortable === false) return
|
|
571
|
+
|
|
572
|
+
let newDirection: 'asc' | 'desc'
|
|
573
|
+
if (sortState.value.column === colIndex) {
|
|
574
|
+
if (sortState.value.direction === 'asc') {
|
|
575
|
+
newDirection = 'desc'
|
|
576
|
+
} else {
|
|
577
|
+
newDirection = 'asc'
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
newDirection = 'asc'
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
sortState.value.column = colIndex
|
|
584
|
+
sortState.value.direction = newDirection
|
|
585
|
+
|
|
586
|
+
// Note: The actual sorting is now handled in the filteredRows computed property
|
|
587
|
+
// This ensures that sorting works on filtered data without modifying the original rows
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const applyFilter = (cellValue: any, filter: FilterState, column: TableColumn): boolean => {
|
|
591
|
+
const filterType = column.filterType || 'text'
|
|
592
|
+
const value = filter.value
|
|
593
|
+
|
|
594
|
+
if (!value && filterType !== 'dateRange' && filterType !== 'checkbox') return true
|
|
595
|
+
|
|
596
|
+
switch (filterType) {
|
|
597
|
+
case 'text': {
|
|
598
|
+
// Handle objects with nested properties
|
|
599
|
+
let searchableText = ''
|
|
600
|
+
if (typeof cellValue === 'object' && cellValue !== null) {
|
|
601
|
+
// If it's an object, search in all string values
|
|
602
|
+
searchableText = Object.values(cellValue as Record<string, unknown>).join(' ')
|
|
603
|
+
} else {
|
|
604
|
+
searchableText = String(cellValue || '')
|
|
605
|
+
}
|
|
606
|
+
return searchableText.toLowerCase().includes(String(value).toLowerCase())
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
case 'number': {
|
|
610
|
+
const numValue = Number(cellValue)
|
|
611
|
+
const filterNum = Number(value)
|
|
612
|
+
return !isNaN(numValue) && !isNaN(filterNum) && numValue === filterNum
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
case 'select':
|
|
616
|
+
return cellValue === value
|
|
617
|
+
|
|
618
|
+
case 'checkbox':
|
|
619
|
+
// For checkbox filter, if checked (true), show only truthy values
|
|
620
|
+
// If unchecked (false/undefined), show all values
|
|
621
|
+
if (value === true) {
|
|
622
|
+
return !!cellValue
|
|
623
|
+
}
|
|
624
|
+
return true
|
|
625
|
+
|
|
626
|
+
case 'date': {
|
|
627
|
+
// Handle both timestamp numbers and date strings
|
|
628
|
+
let cellDate: Date
|
|
629
|
+
if (typeof cellValue === 'number') {
|
|
630
|
+
// Apply the same year transformation as in the format function
|
|
631
|
+
const originalDate = new Date(cellValue)
|
|
632
|
+
const currentYear = new Date().getFullYear()
|
|
633
|
+
cellDate = new Date(currentYear, originalDate.getMonth(), originalDate.getDate())
|
|
634
|
+
} else {
|
|
635
|
+
cellDate = new Date(String(cellValue))
|
|
636
|
+
}
|
|
637
|
+
const filterDate = new Date(String(value))
|
|
638
|
+
return cellDate.toDateString() === filterDate.toDateString()
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
case 'dateRange': {
|
|
642
|
+
const startValue = filter.startValue
|
|
643
|
+
const endValue = filter.endValue
|
|
644
|
+
if (!startValue && !endValue) return true
|
|
645
|
+
|
|
646
|
+
// Handle both timestamp numbers and date strings
|
|
647
|
+
let cellDateRange: Date
|
|
648
|
+
if (typeof cellValue === 'number') {
|
|
649
|
+
// Apply the same year transformation as in the format function
|
|
650
|
+
const originalDate = new Date(cellValue)
|
|
651
|
+
const currentYear = new Date().getFullYear()
|
|
652
|
+
cellDateRange = new Date(currentYear, originalDate.getMonth(), originalDate.getDate())
|
|
653
|
+
} else {
|
|
654
|
+
cellDateRange = new Date(String(cellValue))
|
|
655
|
+
}
|
|
656
|
+
if (startValue && cellDateRange < new Date(String(startValue))) return false
|
|
657
|
+
if (endValue && cellDateRange > new Date(String(endValue))) return false
|
|
658
|
+
|
|
659
|
+
return true
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
default:
|
|
663
|
+
return true
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const setFilter = (colIndex: number, filter: FilterState) => {
|
|
668
|
+
if (!filter.value && !filter.startValue && !filter.endValue) {
|
|
669
|
+
// Remove filter if empty
|
|
670
|
+
delete filterState.value[colIndex]
|
|
671
|
+
} else {
|
|
672
|
+
filterState.value[colIndex] = filter
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const clearFilter = (colIndex: number) => {
|
|
677
|
+
delete filterState.value[colIndex]
|
|
678
|
+
}
|
|
679
|
+
|
|
487
680
|
return {
|
|
488
681
|
// state
|
|
489
682
|
columns,
|
|
@@ -491,13 +684,16 @@ export const createTableStore = (initData: {
|
|
|
491
684
|
connectionHandles,
|
|
492
685
|
connectionPaths,
|
|
493
686
|
display,
|
|
687
|
+
filterState,
|
|
494
688
|
ganttBars,
|
|
495
689
|
modal,
|
|
496
690
|
rows,
|
|
691
|
+
sortState,
|
|
497
692
|
table,
|
|
498
693
|
updates,
|
|
499
694
|
|
|
500
695
|
// getters
|
|
696
|
+
filteredRows,
|
|
501
697
|
hasPinnedColumns,
|
|
502
698
|
isGanttView,
|
|
503
699
|
isTreeView,
|
|
@@ -506,6 +702,7 @@ export const createTableStore = (initData: {
|
|
|
506
702
|
zeroColumn,
|
|
507
703
|
|
|
508
704
|
// actions
|
|
705
|
+
clearFilter,
|
|
509
706
|
closeModal,
|
|
510
707
|
createConnection,
|
|
511
708
|
deleteConnection,
|
|
@@ -524,6 +721,8 @@ export const createTableStore = (initData: {
|
|
|
524
721
|
resizeColumn,
|
|
525
722
|
setCellData,
|
|
526
723
|
setCellText,
|
|
724
|
+
setFilter,
|
|
725
|
+
sortByColumn,
|
|
527
726
|
toggleRowExpand,
|
|
528
727
|
unregisterConnectionHandle,
|
|
529
728
|
unregisterGanttBar,
|
package/src/types/index.ts
CHANGED
|
@@ -72,6 +72,37 @@ export interface TableColumn {
|
|
|
72
72
|
*/
|
|
73
73
|
resizable?: boolean
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Control whether the column should be sortable.
|
|
77
|
+
*
|
|
78
|
+
* @defaultValue true
|
|
79
|
+
*/
|
|
80
|
+
sortable?: boolean
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Control whether the column should be filterable and define filter configuration.
|
|
84
|
+
*
|
|
85
|
+
* @defaultValue true
|
|
86
|
+
*/
|
|
87
|
+
filterable?: boolean
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The type of filter for the column.
|
|
91
|
+
*
|
|
92
|
+
* @defaultValue 'text'
|
|
93
|
+
*/
|
|
94
|
+
filterType?: 'text' | 'select' | 'number' | 'date' | 'dateRange' | 'checkbox' | 'component'
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Options for select-type filters.
|
|
98
|
+
*/
|
|
99
|
+
filterOptions?: any[]
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Custom component for filtering.
|
|
103
|
+
*/
|
|
104
|
+
filterComponent?: string
|
|
105
|
+
|
|
75
106
|
/**
|
|
76
107
|
* The component to use to render the cell for the column. If not provided, the table will
|
|
77
108
|
* render the default `<td>` element.
|