@stonecrop/atable 0.24.0 → 0.26.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/dist/assets/index.css +1 -1
- package/dist/atable.d.ts +88 -0
- package/dist/atable.js +1164 -987
- package/dist/atable.js.map +1 -1
- package/dist/src/composables/table-pagination.d.ts +53 -0
- package/dist/src/composables/table-pagination.d.ts.map +1 -0
- package/dist/src/composables/table-pagination.js +108 -0
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/linkSearchableText.d.ts +7 -0
- package/dist/src/linkSearchableText.d.ts.map +1 -0
- package/dist/src/linkSearchableText.js +44 -0
- package/dist/src/resolveFilterType.d.ts +10 -0
- package/dist/src/resolveFilterType.d.ts.map +1 -0
- package/dist/src/resolveFilterType.js +27 -0
- package/dist/src/stores/table.d.ts +24 -0
- package/dist/src/stores/table.d.ts.map +1 -1
- package/dist/src/stores/table.js +9 -5
- package/dist/src/types/index.d.ts +5 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/components/ATable.vue +46 -3
- package/src/components/ATableColumnFilter.vue +2 -23
- package/src/components/ATablePaginationFooter.vue +100 -0
- package/src/composables/table-pagination.ts +172 -0
- package/src/index.ts +5 -0
- package/src/linkSearchableText.ts +42 -0
- package/src/resolveFilterType.ts +32 -0
- package/src/stores/table.ts +7 -4
- package/src/types/index.ts +6 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<tbody>
|
|
15
15
|
<slot name="body" :data="store">
|
|
16
16
|
<ARow
|
|
17
|
-
v-for="(row, filteredIndex) in
|
|
17
|
+
v-for="(row, filteredIndex) in visibleRows"
|
|
18
18
|
:key="`${row.originalIndex}-${filteredIndex}`"
|
|
19
19
|
:row="row"
|
|
20
20
|
:row-index="row.originalIndex"
|
|
@@ -57,7 +57,20 @@
|
|
|
57
57
|
</slot>
|
|
58
58
|
</tbody>
|
|
59
59
|
|
|
60
|
-
<slot name="footer" :data="store"
|
|
60
|
+
<slot name="footer" :data="store" :page="pagination">
|
|
61
|
+
<ATablePaginationFooter
|
|
62
|
+
:column-count="footerColumnCount"
|
|
63
|
+
:show-footer="showFooter"
|
|
64
|
+
:has-more="hasMore"
|
|
65
|
+
:has-prev="hasPrev"
|
|
66
|
+
:has-next="hasNext"
|
|
67
|
+
:has-local-next="hasLocalNext"
|
|
68
|
+
:has-ever-had-more="hasEverHadMore"
|
|
69
|
+
:loaded-count="loadedCount"
|
|
70
|
+
:loading="paginationLoading"
|
|
71
|
+
:next="paginationNext"
|
|
72
|
+
:prev="paginationPrev" />
|
|
73
|
+
</slot>
|
|
61
74
|
|
|
62
75
|
<!-- Modal overlay -->
|
|
63
76
|
<slot name="modal" :data="store">
|
|
@@ -92,8 +105,10 @@ import AGanttConnection from './AGanttConnection.vue'
|
|
|
92
105
|
import ARow from './ARow.vue'
|
|
93
106
|
import ATableHeader from './ATableHeader.vue'
|
|
94
107
|
import ATableModal from './ATableModal.vue'
|
|
108
|
+
import ATablePaginationFooter from './ATablePaginationFooter.vue'
|
|
109
|
+
import { useTablePagination } from '../composables/table-pagination'
|
|
95
110
|
import { createTableStore } from '../stores/table'
|
|
96
|
-
import type { ColumnSchema } from '@stonecrop/schema'
|
|
111
|
+
import type { ColumnSchema, GetRecordsOptions, GetRecordsResult } from '@stonecrop/schema'
|
|
97
112
|
|
|
98
113
|
import { schemaToColumns } from '../schemaToColumns'
|
|
99
114
|
import type {
|
|
@@ -123,11 +138,15 @@ const {
|
|
|
123
138
|
config = new Object(),
|
|
124
139
|
schema = [],
|
|
125
140
|
linkResolver = undefined,
|
|
141
|
+
getRecords = undefined,
|
|
142
|
+
sourceKey = undefined,
|
|
126
143
|
} = defineProps<{
|
|
127
144
|
id?: string
|
|
128
145
|
config?: TableConfig
|
|
129
146
|
schema?: ColumnSchema[]
|
|
130
147
|
linkResolver?: LinkResolverFn
|
|
148
|
+
getRecords?: (options?: GetRecordsOptions) => Promise<GetRecordsResult>
|
|
149
|
+
sourceKey?: string
|
|
131
150
|
}>()
|
|
132
151
|
|
|
133
152
|
const emit = defineEmits<{
|
|
@@ -156,6 +175,29 @@ const store = createTableStore({
|
|
|
156
175
|
linkResolver: linkResolver ?? injectedLinkResolver,
|
|
157
176
|
})
|
|
158
177
|
|
|
178
|
+
const pagination = useTablePagination({
|
|
179
|
+
rows: () => store.filteredRows,
|
|
180
|
+
pageSize: () => store.config.pageSize,
|
|
181
|
+
getRecords,
|
|
182
|
+
sourceKey: () => sourceKey,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const {
|
|
186
|
+
visibleRows,
|
|
187
|
+
showFooter,
|
|
188
|
+
hasMore,
|
|
189
|
+
hasPrev,
|
|
190
|
+
hasNext,
|
|
191
|
+
hasLocalNext,
|
|
192
|
+
hasEverHadMore,
|
|
193
|
+
loadedCount,
|
|
194
|
+
loading: paginationLoading,
|
|
195
|
+
next: paginationNext,
|
|
196
|
+
prev: paginationPrev,
|
|
197
|
+
} = pagination
|
|
198
|
+
|
|
199
|
+
const footerColumnCount = computed(() => store.columns.length + (store.zeroColumn ? 1 : 0))
|
|
200
|
+
|
|
159
201
|
store.$onAction(({ name, store: storeInstance, args, after }) => {
|
|
160
202
|
if (name === 'setCellData' || name === 'setCellText') {
|
|
161
203
|
const [colIndex, rowIndex, newValue] = args
|
|
@@ -398,6 +440,7 @@ const handleRowAction = (actionType: RowActionType, rowIndex: number, event?: Mo
|
|
|
398
440
|
|
|
399
441
|
defineExpose({
|
|
400
442
|
store,
|
|
443
|
+
pagination,
|
|
401
444
|
createConnection: store.createConnection,
|
|
402
445
|
deleteConnection: store.deleteConnection,
|
|
403
446
|
getConnectionsForBar: store.getConnectionsForBar,
|
|
@@ -66,26 +66,11 @@
|
|
|
66
66
|
|
|
67
67
|
<script setup lang="ts">
|
|
68
68
|
import { ref, reactive, computed } from 'vue'
|
|
69
|
-
import { componentCategory, type ComponentCategory } from '@stonecrop/schema'
|
|
70
69
|
|
|
71
70
|
import { createTableStore } from '../stores/table'
|
|
71
|
+
import { resolveFilterType } from '../resolveFilterType'
|
|
72
72
|
import type { TableColumn } from '../types'
|
|
73
73
|
|
|
74
|
-
// The filter widget is chosen by the component's semantic category.
|
|
75
|
-
const CATEGORY_FILTER: Record<ComponentCategory, 'text' | 'select' | 'number' | 'date' | 'dateRange' | 'checkbox'> = {
|
|
76
|
-
text: 'text',
|
|
77
|
-
number: 'number',
|
|
78
|
-
boolean: 'checkbox',
|
|
79
|
-
date: 'date',
|
|
80
|
-
datetime: 'dateRange',
|
|
81
|
-
select: 'select',
|
|
82
|
-
code: 'text',
|
|
83
|
-
link: 'text',
|
|
84
|
-
attach: 'text',
|
|
85
|
-
quantity: 'number',
|
|
86
|
-
currency: 'number',
|
|
87
|
-
}
|
|
88
|
-
|
|
89
74
|
const { column, colIndex, store } = defineProps<{
|
|
90
75
|
column: TableColumn
|
|
91
76
|
colIndex: number
|
|
@@ -98,13 +83,7 @@ const dateFilter = reactive({
|
|
|
98
83
|
endValue: '',
|
|
99
84
|
})
|
|
100
85
|
|
|
101
|
-
const resolvedFilterType = computed(() =>
|
|
102
|
-
if (column.filterType) return column.filterType
|
|
103
|
-
const category = componentCategory(column.component)
|
|
104
|
-
if (category) return CATEGORY_FILTER[category]
|
|
105
|
-
// An unknown (custom) component gets the widget that can filter anything.
|
|
106
|
-
return 'text'
|
|
107
|
-
})
|
|
86
|
+
const resolvedFilterType = computed(() => resolveFilterType(column))
|
|
108
87
|
|
|
109
88
|
const getSelectOptions = (col: TableColumn): any[] => {
|
|
110
89
|
if (col.filterOptions) return col.filterOptions
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tfoot v-if="showFooter" class="atable-pagination">
|
|
3
|
+
<tr>
|
|
4
|
+
<td :colspan="columnCount">
|
|
5
|
+
<p v-if="hasMore" class="truncation-note">This is a partial list — more records exist on the server.</p>
|
|
6
|
+
<p v-else-if="showCompleteNote" class="completion-note">All {{ loadedCount }} records loaded.</p>
|
|
7
|
+
<div v-if="showControls" class="atable-pagination-controls">
|
|
8
|
+
<button type="button" class="atable-pagination-btn" :disabled="!hasPrev || loading" @click="prev">
|
|
9
|
+
Previous
|
|
10
|
+
</button>
|
|
11
|
+
<button type="button" class="atable-pagination-btn" :disabled="!hasNext || loading" @click="onNext">
|
|
12
|
+
{{ hasMore && !hasLocalNext ? 'Load more' : 'Next' }}
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</td>
|
|
16
|
+
</tr>
|
|
17
|
+
</tfoot>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
import { computed } from 'vue'
|
|
22
|
+
|
|
23
|
+
const props = defineProps<{
|
|
24
|
+
columnCount: number
|
|
25
|
+
showFooter: boolean
|
|
26
|
+
hasMore: boolean
|
|
27
|
+
hasPrev: boolean
|
|
28
|
+
hasNext: boolean
|
|
29
|
+
hasLocalNext: boolean
|
|
30
|
+
hasEverHadMore: boolean
|
|
31
|
+
loadedCount: number
|
|
32
|
+
loading: boolean
|
|
33
|
+
next: () => Promise<void>
|
|
34
|
+
prev: () => void
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const showCompleteNote = computed(() => !props.hasMore && props.hasEverHadMore)
|
|
38
|
+
const showControls = computed(() => props.hasMore || props.hasPrev)
|
|
39
|
+
|
|
40
|
+
const onNext = () => {
|
|
41
|
+
void props.next()
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.atable-pagination td {
|
|
47
|
+
padding: 0.75rem 0.5ch;
|
|
48
|
+
border-top: 1px solid var(--sc-row-border-color, #e0e0e0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.truncation-note,
|
|
52
|
+
.completion-note {
|
|
53
|
+
margin: 0 0 0.5rem;
|
|
54
|
+
font-size: var(--sc-table-font-size, 0.875rem);
|
|
55
|
+
color: var(--sc-gray-60, #666);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.completion-note {
|
|
59
|
+
margin-bottom: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.atable-pagination-controls {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.atable-pagination-btn {
|
|
68
|
+
appearance: none;
|
|
69
|
+
-webkit-appearance: none;
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
padding: 0.5rem 1rem;
|
|
72
|
+
background: var(--sc-btn-color, #fafafa);
|
|
73
|
+
color: var(--sc-btn-label-color, #333333);
|
|
74
|
+
border: 1px solid var(--sc-btn-border, #cccccc);
|
|
75
|
+
border-radius: var(--sc-border-radius, 0);
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
font-family: var(--sc-font-family, Arial, sans-serif);
|
|
78
|
+
font-size: var(--sc-table-font-size, 0.875rem);
|
|
79
|
+
font-weight: 400;
|
|
80
|
+
line-height: 1.2;
|
|
81
|
+
transition: background-color 0.15s ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.atable-pagination-btn:hover:not(:disabled) {
|
|
85
|
+
background: var(--sc-btn-hover, #f2f2f2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.atable-pagination-btn:focus-visible {
|
|
89
|
+
outline: 2px solid var(--sc-input-active-border-color, #333333);
|
|
90
|
+
outline-offset: 1px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.atable-pagination-btn:disabled {
|
|
94
|
+
cursor: not-allowed;
|
|
95
|
+
background: var(--sc-gray-5, #f2f2f2);
|
|
96
|
+
color: var(--sc-gray-50, #808080);
|
|
97
|
+
border-color: var(--sc-gray-20, #cccccc);
|
|
98
|
+
opacity: 1;
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { GetRecordsOptions, GetRecordsResult } from '@stonecrop/schema'
|
|
2
|
+
import { computed, nextTick, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { TableRow } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A table row after filter/sort, with its index in the held row list.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type FilteredTableRow = TableRow & { originalIndex: number }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for {@link useTablePagination}.
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export interface UseTablePaginationOptions {
|
|
17
|
+
/** Rows after filter and sort — pagination is a window over this list, not part of filtering. */
|
|
18
|
+
rows: MaybeRefOrGetter<FilteredTableRow[]>
|
|
19
|
+
/** When set, slice the held rows into windows of this size. Omit to show the whole held list. */
|
|
20
|
+
pageSize?: MaybeRefOrGetter<number | undefined>
|
|
21
|
+
/** Fetch the next server page. Typed from schema so ATable need not import Stonecrop. */
|
|
22
|
+
getRecords?: (options?: GetRecordsOptions) => Promise<GetRecordsResult>
|
|
23
|
+
/** When this changes, refetch from offset 0. */
|
|
24
|
+
sourceKey?: MaybeRefOrGetter<string | undefined>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pagination state and controls returned by {@link useTablePagination}.
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface TablePagination {
|
|
32
|
+
visibleRows: Ref<FilteredTableRow[]>
|
|
33
|
+
hasPrev: Ref<boolean>
|
|
34
|
+
hasNext: Ref<boolean>
|
|
35
|
+
/** Whether the server reported more records beyond what has been fetched. */
|
|
36
|
+
hasMore: Ref<boolean>
|
|
37
|
+
loading: Ref<boolean>
|
|
38
|
+
next: () => Promise<void>
|
|
39
|
+
prev: () => void
|
|
40
|
+
/** Whether footer chrome should render. */
|
|
41
|
+
showFooter: Ref<boolean>
|
|
42
|
+
/** Whether another in-memory window exists before asking the server. */
|
|
43
|
+
hasLocalNext: Ref<boolean>
|
|
44
|
+
/** Rows currently held after filter/sort — useful for a completion summary. */
|
|
45
|
+
loadedCount: Ref<number>
|
|
46
|
+
/** True once the server reported a further page existed (multi-page fetch). */
|
|
47
|
+
hasEverHadMore: Ref<boolean>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Window rows after filter/sort, and optionally walk {@link @stonecrop/schema#GetRecordsOptions} for the next page.
|
|
52
|
+
* Pagination is not a filter — pass already-filtered rows.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export function useTablePagination(options: UseTablePaginationOptions): TablePagination {
|
|
56
|
+
const heldRows = computed(() => toValue(options.rows))
|
|
57
|
+
const pageSize = computed(() => toValue(options.pageSize))
|
|
58
|
+
const sourceKey = computed(() => toValue(options.sourceKey))
|
|
59
|
+
|
|
60
|
+
const windowIndex = ref(0)
|
|
61
|
+
const loading = ref(false)
|
|
62
|
+
const serverHasMore = ref(false)
|
|
63
|
+
const hasEverHadMore = ref(false)
|
|
64
|
+
const lastFetchOffset = ref(0)
|
|
65
|
+
const lastPageLength = ref(0)
|
|
66
|
+
const loadedCount = computed(() => heldRows.value.length)
|
|
67
|
+
|
|
68
|
+
const effectivePageSize = computed(() => {
|
|
69
|
+
const size = pageSize.value
|
|
70
|
+
if (size != null && size > 0) {
|
|
71
|
+
return size
|
|
72
|
+
}
|
|
73
|
+
// No pageSize: one window over everything held (server list or full child table).
|
|
74
|
+
const count = heldRows.value.length
|
|
75
|
+
return count > 0 ? count : 1
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const windowCount = computed(() => Math.max(1, Math.ceil(heldRows.value.length / effectivePageSize.value)))
|
|
79
|
+
|
|
80
|
+
const visibleRows = computed(() => {
|
|
81
|
+
const start = windowIndex.value * effectivePageSize.value
|
|
82
|
+
return heldRows.value.slice(start, start + effectivePageSize.value)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const hasLocalNext = computed(() => windowIndex.value < windowCount.value - 1)
|
|
86
|
+
const hasPrev = computed(() => windowIndex.value > 0)
|
|
87
|
+
const hasNext = computed(() => hasLocalNext.value || (Boolean(options.getRecords) && serverHasMore.value))
|
|
88
|
+
const hasMore = computed(() => serverHasMore.value)
|
|
89
|
+
|
|
90
|
+
const showFooter = computed(() => {
|
|
91
|
+
if (options.getRecords) {
|
|
92
|
+
// Keep the footer after the last "Load more" so completion is visible, not silent.
|
|
93
|
+
return serverHasMore.value || hasPrev.value || loading.value || (hasEverHadMore.value && !serverHasMore.value)
|
|
94
|
+
}
|
|
95
|
+
const size = pageSize.value
|
|
96
|
+
return size != null && size > 0 && heldRows.value.length > size
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const resetWindow = () => {
|
|
100
|
+
windowIndex.value = 0
|
|
101
|
+
hasEverHadMore.value = false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fetchFromServer = async (fetchOptions?: GetRecordsOptions) => {
|
|
105
|
+
if (!options.getRecords) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
loading.value = true
|
|
109
|
+
try {
|
|
110
|
+
const result = await options.getRecords(fetchOptions)
|
|
111
|
+
serverHasMore.value = result.hasMore
|
|
112
|
+
if (result.hasMore) {
|
|
113
|
+
hasEverHadMore.value = true
|
|
114
|
+
}
|
|
115
|
+
lastFetchOffset.value = fetchOptions?.offset ?? 0
|
|
116
|
+
lastPageLength.value = result.data.length
|
|
117
|
+
} finally {
|
|
118
|
+
loading.value = false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
watch(
|
|
123
|
+
() => sourceKey.value,
|
|
124
|
+
() => {
|
|
125
|
+
resetWindow()
|
|
126
|
+
if (options.getRecords) {
|
|
127
|
+
void fetchFromServer()
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{ immediate: true }
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
watch(heldRows, () => {
|
|
134
|
+
// Shrinking the held set can leave the window past the end.
|
|
135
|
+
if (windowIndex.value >= windowCount.value) {
|
|
136
|
+
windowIndex.value = Math.max(0, windowCount.value - 1)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const next = async () => {
|
|
141
|
+
if (hasLocalNext.value) {
|
|
142
|
+
windowIndex.value += 1
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (options.getRecords && serverHasMore.value) {
|
|
146
|
+
await fetchFromServer({ offset: lastFetchOffset.value + lastPageLength.value })
|
|
147
|
+
await nextTick()
|
|
148
|
+
// Parent/HST grows rows; show the newly appended tail.
|
|
149
|
+
windowIndex.value = Math.max(0, windowCount.value - 1)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const prev = () => {
|
|
154
|
+
if (windowIndex.value > 0) {
|
|
155
|
+
windowIndex.value -= 1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
visibleRows,
|
|
161
|
+
hasPrev,
|
|
162
|
+
hasNext,
|
|
163
|
+
hasMore,
|
|
164
|
+
loading,
|
|
165
|
+
next,
|
|
166
|
+
prev,
|
|
167
|
+
showFooter,
|
|
168
|
+
hasLocalNext,
|
|
169
|
+
loadedCount,
|
|
170
|
+
hasEverHadMore,
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,10 @@ import ATableHeader from './components/ATableHeader.vue'
|
|
|
9
9
|
import ATableLoading from './components/ATableLoading.vue'
|
|
10
10
|
import ATableLoadingBar from './components/ATableLoadingBar.vue'
|
|
11
11
|
import ATableModal from './components/ATableModal.vue'
|
|
12
|
+
import ATablePaginationFooter from './components/ATablePaginationFooter.vue'
|
|
12
13
|
export { createTableStore } from './stores/table'
|
|
14
|
+
export { useTablePagination } from './composables/table-pagination'
|
|
15
|
+
export type { FilteredTableRow, TablePagination, UseTablePaginationOptions } from './composables/table-pagination'
|
|
13
16
|
export type { FilterState, FilterStateRecord } from './stores/table'
|
|
14
17
|
export type * from './types'
|
|
15
18
|
export { schemaToColumns } from './schemaToColumns'
|
|
@@ -41,6 +44,7 @@ function install(app: App /* options */) {
|
|
|
41
44
|
app.component('ATableLoading', ATableLoading)
|
|
42
45
|
app.component('ATableLoadingBar', ATableLoadingBar)
|
|
43
46
|
app.component('ATableModal', ATableModal)
|
|
47
|
+
app.component('ATablePaginationFooter', ATablePaginationFooter)
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export {
|
|
@@ -53,5 +57,6 @@ export {
|
|
|
53
57
|
ATableLoading,
|
|
54
58
|
ATableLoadingBar,
|
|
55
59
|
ATableModal,
|
|
60
|
+
ATablePaginationFooter,
|
|
56
61
|
install,
|
|
57
62
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a scalar cell value to a non-empty string, or nothing.
|
|
3
|
+
*
|
|
4
|
+
* Rejecting objects is what the haystack wants: a nested value has no single textual form, and
|
|
5
|
+
* `String()` on one contributes `[object Object]`, which matches a search for "object" and nothing
|
|
6
|
+
* a user would actually type.
|
|
7
|
+
*/
|
|
8
|
+
function scalarText(value: unknown): string | undefined {
|
|
9
|
+
if (value == null || typeof value === 'object') return undefined
|
|
10
|
+
if (typeof value === 'string') return value === '' ? undefined : value
|
|
11
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
12
|
+
return String(value)
|
|
13
|
+
}
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the substring-search haystack for a link column cell.
|
|
19
|
+
* Server middleware enriches inline FKs to `{ id, displayText }`; search both.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export function linkSearchableText(cellValue: unknown): string {
|
|
23
|
+
if (cellValue == null || cellValue === '') return ''
|
|
24
|
+
|
|
25
|
+
// `in` rather than an assertion to `Record<string, unknown>`: the assertion claims the value has
|
|
26
|
+
// string keys of unknown type, which is narrower than `object` and so is a claim this function
|
|
27
|
+
// cannot make about a cell it did not build.
|
|
28
|
+
if (typeof cellValue === 'object') {
|
|
29
|
+
const parts: string[] = []
|
|
30
|
+
if ('id' in cellValue) {
|
|
31
|
+
const id = scalarText(cellValue.id)
|
|
32
|
+
if (id !== undefined) parts.push(id)
|
|
33
|
+
}
|
|
34
|
+
if ('displayText' in cellValue) {
|
|
35
|
+
const displayText = scalarText(cellValue.displayText)
|
|
36
|
+
if (displayText !== undefined) parts.push(displayText)
|
|
37
|
+
}
|
|
38
|
+
return parts.join(' ')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return scalarText(cellValue) ?? ''
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { componentCategory, type ComponentCategory } from '@stonecrop/schema'
|
|
2
|
+
|
|
3
|
+
import type { TableColumn } from './types'
|
|
4
|
+
|
|
5
|
+
/** Filter widget type derived from a component's semantic category. */
|
|
6
|
+
export type ResolvedFilterType = 'text' | 'select' | 'number' | 'date' | 'dateRange' | 'checkbox' | 'component'
|
|
7
|
+
|
|
8
|
+
const CATEGORY_FILTER: Record<ComponentCategory, Exclude<ResolvedFilterType, 'component'>> = {
|
|
9
|
+
text: 'text',
|
|
10
|
+
number: 'number',
|
|
11
|
+
boolean: 'checkbox',
|
|
12
|
+
date: 'date',
|
|
13
|
+
datetime: 'dateRange',
|
|
14
|
+
select: 'select',
|
|
15
|
+
code: 'text',
|
|
16
|
+
link: 'text',
|
|
17
|
+
attach: 'text',
|
|
18
|
+
quantity: 'number',
|
|
19
|
+
currency: 'number',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the filter widget and filter logic type for a column.
|
|
24
|
+
* Explicit `filterType` wins; otherwise derive from the component category.
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function resolveFilterType(column: Pick<TableColumn, 'filterType' | 'component'>): ResolvedFilterType {
|
|
28
|
+
if (column.filterType) return column.filterType
|
|
29
|
+
const category = componentCategory(column.component)
|
|
30
|
+
if (category) return CATEGORY_FILTER[category]
|
|
31
|
+
return 'text'
|
|
32
|
+
}
|
package/src/stores/table.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { componentCategory } from '@stonecrop/schema'
|
|
|
3
3
|
import type { BadgeDescriptor } from '@stonecrop/schema'
|
|
4
4
|
import { type CSSProperties, computed, ref } from 'vue'
|
|
5
5
|
|
|
6
|
+
import { linkSearchableText } from '../linkSearchableText'
|
|
6
7
|
import type {
|
|
7
8
|
CellContext,
|
|
8
9
|
ConnectionHandle,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
TableModal,
|
|
16
17
|
TableRow,
|
|
17
18
|
} from '../types'
|
|
19
|
+
import { resolveFilterType } from '../resolveFilterType'
|
|
18
20
|
import { formatCurrency, formatQuantity, generateHash } from '../utils'
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -77,15 +79,16 @@ function isNodeOpen(rowIndex: number, treeDisplay: TableDisplay[]): boolean {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
function applyFilter(cellValue: any, filter: FilterState, column: TableColumn): boolean {
|
|
80
|
-
const filterType = column
|
|
82
|
+
const filterType = resolveFilterType(column)
|
|
81
83
|
const value = filter.value
|
|
82
84
|
|
|
83
85
|
if (!value && filterType !== 'dateRange' && filterType !== 'checkbox') return true
|
|
84
86
|
|
|
85
87
|
switch (filterType) {
|
|
86
88
|
case 'text': {
|
|
87
|
-
const searchableText =
|
|
88
|
-
|
|
89
|
+
const searchableText = column.linkDoctype
|
|
90
|
+
? linkSearchableText(cellValue)
|
|
91
|
+
: typeof cellValue === 'object' && cellValue !== null
|
|
89
92
|
? Object.values(cellValue).join(' ')
|
|
90
93
|
: String(cellValue || '')
|
|
91
94
|
return searchableText.toLowerCase().includes(String(value).toLowerCase())
|
|
@@ -360,7 +363,7 @@ export const createTableStore = (initData: {
|
|
|
360
363
|
filter.value ||
|
|
361
364
|
filter.startValue ||
|
|
362
365
|
filter.endValue ||
|
|
363
|
-
(column
|
|
366
|
+
(resolveFilterType(column) === 'checkbox' && filter.value !== undefined)
|
|
364
367
|
|
|
365
368
|
if (!hasFilterValue) return
|
|
366
369
|
|
package/src/types/index.ts
CHANGED
|
@@ -205,6 +205,12 @@ export interface BaseTableConfig {
|
|
|
205
205
|
* Configuration for row-level actions (add, delete, duplicate, etc.).
|
|
206
206
|
*/
|
|
207
207
|
rowActions?: RowActionsConfig
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* When set, slice the held rows into in-memory windows of this size.
|
|
211
|
+
* Omit to show the whole held list as one window (server paging uses the backend cap).
|
|
212
|
+
*/
|
|
213
|
+
pageSize?: number
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
/**
|