@katlux/providers 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +16 -0
- package/build.config.ts +4 -0
- package/package.json +24 -0
- package/services/CacheProvider/ACacheProvider.ts +13 -0
- package/services/CacheProvider/CApplicationCache.ts +123 -0
- package/services/CacheProvider/CCookieCache.ts +212 -0
- package/services/CacheProvider/CIndexedDBCache.ts +312 -0
- package/services/CacheProvider/CLocalStorageCache.ts +232 -0
- package/services/CacheProvider/CMemoryCache.ts +185 -0
- package/services/CacheProvider/CSessionCache.ts +378 -0
- package/services/CacheProvider/CacheProviderFactory.ts +22 -0
- package/services/DataProvider/AAPIDataProvider.ts +24 -0
- package/services/DataProvider/ADataProvider.ts +126 -0
- package/services/DataProvider/CAPIFlatClientDataProvider.ts +199 -0
- package/services/DataProvider/CAPIFlatServerDataProvider.ts +77 -0
- package/services/DataProvider/CAPITreeClientDataProvider.ts +205 -0
- package/services/DataProvider/CAPITreeServerDataProvider.ts +98 -0
- package/services/DataProvider/CFlatClientDataProvider.ts +52 -0
- package/services/DataProvider/CFlatServerDataProvider.ts +104 -0
- package/services/DataProvider/CTreeClientDataProvider.ts +335 -0
- package/services/DataProvider/CTreeServerDataProvider.ts +207 -0
- package/services/RequestProvider/RequestProvider.ts +165 -0
- package/services/RequestProvider/serverCache.ts +24 -0
- package/services/index.ts +19 -0
- package/services/types.ts +172 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import { ADataProvider } from './ADataProvider'
|
|
3
|
+
import type { TDataRow, IDataFilter, IDataProviderOptions } from '../types'
|
|
4
|
+
import { useFilterLogic } from '@katlux/toolkit/composables/useFilterLogic'
|
|
5
|
+
import { useSortLogic } from '@katlux/toolkit/composables/useSortLogic'
|
|
6
|
+
|
|
7
|
+
const { evaluateFilter, filterData } = useFilterLogic()
|
|
8
|
+
const { sortData } = useSortLogic()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export class CFlatClientDataProvider extends ADataProvider {
|
|
12
|
+
allData: Ref<TDataRow[]> = ref([])
|
|
13
|
+
filteredData: Ref<TDataRow[]> = ref([])
|
|
14
|
+
|
|
15
|
+
constructor(options?: IDataProviderOptions) {
|
|
16
|
+
super(options)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
setData(data: TDataRow[]) {
|
|
20
|
+
this.allData.value = [...data]
|
|
21
|
+
this.loadPageData()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async loadPageData(): Promise<void> {
|
|
25
|
+
|
|
26
|
+
// Apply filters
|
|
27
|
+
if (!this.filter.value) {
|
|
28
|
+
this.filteredData.value = this.allData.value
|
|
29
|
+
} else {
|
|
30
|
+
const { results } = filterData(this.allData.value, this.filter.value)
|
|
31
|
+
this.filteredData.value = results
|
|
32
|
+
}
|
|
33
|
+
this.rowCount.value = this.filteredData.value.length
|
|
34
|
+
|
|
35
|
+
// Apply sort
|
|
36
|
+
if (this.sortList.value.length > 0)
|
|
37
|
+
this.filteredData.value = sortData(this.sortList.value, this.filteredData.value)
|
|
38
|
+
// Apply pagination (pageSize <= 0 means no pagination)
|
|
39
|
+
if (this.pageSize.value <= 0) {
|
|
40
|
+
this.pageData.value = this.filteredData.value
|
|
41
|
+
} else {
|
|
42
|
+
const startIndex = (this.currentPage.value - 1) * this.pageSize.value
|
|
43
|
+
const endIndex = startIndex + this.pageSize.value
|
|
44
|
+
this.pageData.value = this.filteredData.value.slice(startIndex, endIndex)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
refreshData(): void {
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import { ADataProvider } from './ADataProvider'
|
|
3
|
+
import type { TDataRow, IDataResult, TPageDataHandler, IDataFilter, IDataSort, IDataProviderOptions } from '../types'
|
|
4
|
+
|
|
5
|
+
export class CFlatServerDataProvider extends ADataProvider {
|
|
6
|
+
|
|
7
|
+
pageDataHandler: Ref<TPageDataHandler> = ref((currentPage: number, pageSize: number, filter: IDataFilter, sortList: IDataSort[]): IDataResult | Promise<IDataResult> => {
|
|
8
|
+
return {
|
|
9
|
+
rows: [],
|
|
10
|
+
rowCount: 0
|
|
11
|
+
};
|
|
12
|
+
})
|
|
13
|
+
contextKey: Ref<string> = ref("")
|
|
14
|
+
initialLoad: Ref<boolean> = ref(true)
|
|
15
|
+
|
|
16
|
+
constructor(options?: IDataProviderOptions) {
|
|
17
|
+
super(options)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setContextKey(key: string) {
|
|
21
|
+
this.contextKey.value = key
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async setPageDataHandler(handler: TPageDataHandler) {
|
|
25
|
+
this.pageDataHandler.value = handler
|
|
26
|
+
await this.loadPageData()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadPageData(options?: { disableCache?: boolean }): Promise<void> {
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if (!this.pageDataHandler.value || (!this.SSR.value && import.meta.server)) return;
|
|
33
|
+
|
|
34
|
+
// Stable key generation for Nuxt hydration
|
|
35
|
+
const stableStringify = (val: any) => JSON.stringify(val === undefined ? "" : val)
|
|
36
|
+
const key = [
|
|
37
|
+
this.contextKey.value,
|
|
38
|
+
this.currentPage.value,
|
|
39
|
+
this.pageSize.value,
|
|
40
|
+
stableStringify(this.filter.value),
|
|
41
|
+
stableStringify(this.sortList.value)
|
|
42
|
+
].join('-')
|
|
43
|
+
|
|
44
|
+
const fetcher = async () => await this.pageDataHandler.value(this.currentPage.value, this.pageSize.value, this.filter.value as IDataFilter, this.sortList.value as IDataSort[], options)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// Check for existing data from SSR payload first (Only if SSR is enabled)
|
|
48
|
+
// Skip if disableCache is true
|
|
49
|
+
if (!options?.disableCache && this.SSR.value && this.contextKey.value && this.initialLoad.value) {
|
|
50
|
+
const { data } = useNuxtData(key)
|
|
51
|
+
if (data.value) {
|
|
52
|
+
const result = data.value as IDataResult
|
|
53
|
+
this.pageData.value = result.rows
|
|
54
|
+
this.rowCount.value = result.rowCount
|
|
55
|
+
this.initialLoad.value = false
|
|
56
|
+
this.loading.value = false
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// SSR: use useAsyncData when SSR is enabled (works on both server and client)
|
|
62
|
+
if (this.SSR.value && this.contextKey.value) {
|
|
63
|
+
this.loading.value = true
|
|
64
|
+
const { data } = await useAsyncData(key, fetcher)
|
|
65
|
+
if (data.value) {
|
|
66
|
+
const result = data.value as IDataResult
|
|
67
|
+
this.pageData.value = result.rows
|
|
68
|
+
this.rowCount.value = result.rowCount
|
|
69
|
+
this.initialLoad.value = false
|
|
70
|
+
this.loading.value = false
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// For non-SSR client-side, handle asynchronously without blocking
|
|
76
|
+
if (!this.SSR.value) {
|
|
77
|
+
// Call fetcher and handle promise asynchronously after Nuxt is ready (hydrated)
|
|
78
|
+
// This avoids hydration mismatches on initial load
|
|
79
|
+
onNuxtReady(() => {
|
|
80
|
+
this.loading.value = true
|
|
81
|
+
fetcher().then((returnedData) => {
|
|
82
|
+
this.pageData.value = returnedData.rows
|
|
83
|
+
this.rowCount.value = returnedData.rowCount
|
|
84
|
+
this.initialLoad.value = false
|
|
85
|
+
this.loading.value = false
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
this.initialLoad.value = false
|
|
88
|
+
this.loading.value = false
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// SSR client-side or fallback: await the request
|
|
95
|
+
this.loading.value = true
|
|
96
|
+
const returnedData: IDataResult = await fetcher()
|
|
97
|
+
this.pageData.value = returnedData.rows
|
|
98
|
+
this.rowCount.value = returnedData.rowCount
|
|
99
|
+
this.initialLoad.value = false
|
|
100
|
+
this.loading.value = false
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import { CFlatClientDataProvider } from './CFlatClientDataProvider'
|
|
3
|
+
import { useFilterLogic } from '@katlux/toolkit/composables/useFilterLogic'
|
|
4
|
+
import type {
|
|
5
|
+
TDataRow,
|
|
6
|
+
ITreeNode,
|
|
7
|
+
ITreeNodeStatic,
|
|
8
|
+
TreePaginationMode,
|
|
9
|
+
ITreeDataProviderOptions
|
|
10
|
+
} from '../types'
|
|
11
|
+
|
|
12
|
+
const { evaluateFilter, filterData } = useFilterLogic()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export class CTreeClientDataProvider extends CFlatClientDataProvider {
|
|
16
|
+
// Tree configuration
|
|
17
|
+
parentKey: string = 'parentId'
|
|
18
|
+
idKey: string = 'id'
|
|
19
|
+
expandedByDefault: boolean = false
|
|
20
|
+
paginateBy: TreePaginationMode = 'all'
|
|
21
|
+
|
|
22
|
+
// Tree state
|
|
23
|
+
expandedNodes: Ref<Set<string | number>> = ref(new Set())
|
|
24
|
+
|
|
25
|
+
// Internal: static tree structure (only rebuilds when data changes)
|
|
26
|
+
private _staticNodes: ComputedRef<ITreeNodeStatic[]>
|
|
27
|
+
private _nodeMap: ComputedRef<Map<string | number, ITreeNodeStatic>>
|
|
28
|
+
private _sortedNodeIds: ComputedRef<(string | number)[]>
|
|
29
|
+
|
|
30
|
+
constructor(options?: ITreeDataProviderOptions) {
|
|
31
|
+
super(options)
|
|
32
|
+
if (options?.parentKey) this.parentKey = options.parentKey
|
|
33
|
+
if (options?.idKey) this.idKey = options.idKey
|
|
34
|
+
if (options?.expandedByDefault) this.expandedByDefault = options.expandedByDefault
|
|
35
|
+
if (options?.paginateBy) this.paginateBy = options.paginateBy
|
|
36
|
+
|
|
37
|
+
// Static tree structure - only rebuilds when allData changes
|
|
38
|
+
this._staticNodes = computed(() => this.buildStaticNodes())
|
|
39
|
+
this._nodeMap = computed(() => new Map(this._staticNodes.value.map(n => [n.id, n])))
|
|
40
|
+
this._sortedNodeIds = computed(() => this.computeSortedIds())
|
|
41
|
+
|
|
42
|
+
// Watch for changes that require pageData update
|
|
43
|
+
// Note: currentPage and pageSize changes are handled by parent ADataProvider watchers
|
|
44
|
+
// which call loadPageData() -> updatePageData()
|
|
45
|
+
watch([this.expandedNodes, this._staticNodes], () => {
|
|
46
|
+
this.updatePageData()
|
|
47
|
+
}, { immediate: true })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private buildStaticNodes(): ITreeNodeStatic[] {
|
|
51
|
+
const allData = this.allData.value || []
|
|
52
|
+
const nodeMap = new Map<string | number, ITreeNodeStatic>()
|
|
53
|
+
const childrenMap = new Map<string | number, TDataRow[]>()
|
|
54
|
+
|
|
55
|
+
// First pass: create nodes and map children
|
|
56
|
+
allData.forEach((item: TDataRow) => {
|
|
57
|
+
const id = item[this.idKey]
|
|
58
|
+
const parentId = item[this.parentKey] || null
|
|
59
|
+
|
|
60
|
+
nodeMap.set(id, {
|
|
61
|
+
data: item,
|
|
62
|
+
id,
|
|
63
|
+
parentId,
|
|
64
|
+
depth: 0,
|
|
65
|
+
hasChildren: false,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (parentId !== null) {
|
|
69
|
+
if (!childrenMap.has(parentId)) {
|
|
70
|
+
childrenMap.set(parentId, [])
|
|
71
|
+
}
|
|
72
|
+
childrenMap.get(parentId)!.push(item)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Second pass: set hasChildren and calculate depth
|
|
77
|
+
const calculateDepth = (nodeId: string | number, depth: number = 0): void => {
|
|
78
|
+
const node = nodeMap.get(nodeId)
|
|
79
|
+
if (!node) return
|
|
80
|
+
|
|
81
|
+
node.depth = depth
|
|
82
|
+
const children = childrenMap.get(nodeId)
|
|
83
|
+
if (children && children.length > 0) {
|
|
84
|
+
node.hasChildren = true
|
|
85
|
+
children.forEach((child: TDataRow) => {
|
|
86
|
+
calculateDepth(child[this.idKey], depth + 1)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
nodeMap.forEach((node) => {
|
|
92
|
+
if (node.parentId === null) {
|
|
93
|
+
calculateDepth(node.id, 0)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return Array.from(nodeMap.values())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pre-compute sorted order once (only when data changes)
|
|
101
|
+
private computeSortedIds(): (string | number)[] {
|
|
102
|
+
const nodes = this._staticNodes.value
|
|
103
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]))
|
|
104
|
+
|
|
105
|
+
// Build tree order using DFS
|
|
106
|
+
const sortedIds: (string | number)[] = []
|
|
107
|
+
const rootNodes = nodes.filter(n => n.parentId === null)
|
|
108
|
+
|
|
109
|
+
// Get children map
|
|
110
|
+
const childrenMap = new Map<string | number, ITreeNodeStatic[]>()
|
|
111
|
+
nodes.forEach(n => {
|
|
112
|
+
if (n.parentId !== null) {
|
|
113
|
+
if (!childrenMap.has(n.parentId)) {
|
|
114
|
+
childrenMap.set(n.parentId, [])
|
|
115
|
+
}
|
|
116
|
+
childrenMap.get(n.parentId)!.push(n)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const collectNodes = (node: ITreeNodeStatic): void => {
|
|
121
|
+
sortedIds.push(node.id)
|
|
122
|
+
const children = childrenMap.get(node.id) || []
|
|
123
|
+
children.forEach(child => collectNodes(child))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
rootNodes.forEach(root => collectNodes(root))
|
|
127
|
+
return sortedIds
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fast visibility check and page data update
|
|
131
|
+
private updatePageData(): void {
|
|
132
|
+
const nodeMap = this._nodeMap.value
|
|
133
|
+
const sortedIds = this._sortedNodeIds.value
|
|
134
|
+
const visibleNodes: ITreeNode[] = []
|
|
135
|
+
|
|
136
|
+
if (this.filter.value && this.filter.value.active) {
|
|
137
|
+
// FILTER LOGIC
|
|
138
|
+
// Use shared filterData with treeOptions
|
|
139
|
+
// Static nodes are already in flat format needed
|
|
140
|
+
const { results: filtered, expandedNodes: expandedResults } = filterData(
|
|
141
|
+
this._staticNodes.value,
|
|
142
|
+
this.filter.value,
|
|
143
|
+
{
|
|
144
|
+
treeOptions: {
|
|
145
|
+
idKey: this.idKey,
|
|
146
|
+
parentKey: this.parentKey
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// We need to update isExpanded based on the result
|
|
152
|
+
const expandedSet = expandedResults || new Set()
|
|
153
|
+
for (const node of (filtered as ITreeNodeStatic[])) {
|
|
154
|
+
visibleNodes.push({
|
|
155
|
+
...node,
|
|
156
|
+
isExpanded: expandedSet.has(node.id)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
} else {
|
|
161
|
+
// STANDARD TREE VISIBILITY LOGIC
|
|
162
|
+
for (const id of sortedIds) {
|
|
163
|
+
const staticNode = nodeMap.get(id)
|
|
164
|
+
if (!staticNode) continue
|
|
165
|
+
|
|
166
|
+
// Check if node is visible (all ancestors expanded)
|
|
167
|
+
if (this.isNodeVisible(staticNode, nodeMap)) {
|
|
168
|
+
const isExpanded = this.expandedByDefault
|
|
169
|
+
? !this.expandedNodes.value.has(id)
|
|
170
|
+
: this.expandedNodes.value.has(id)
|
|
171
|
+
|
|
172
|
+
visibleNodes.push({
|
|
173
|
+
...staticNode,
|
|
174
|
+
isExpanded
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply pagination
|
|
181
|
+
const paginatedNodes = this.paginateNodes(visibleNodes)
|
|
182
|
+
|
|
183
|
+
// Update pageData for KDatatable
|
|
184
|
+
this.pageData.value = paginatedNodes.map(node => ({
|
|
185
|
+
...node.data,
|
|
186
|
+
__treeNode: node
|
|
187
|
+
}))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private isNodeVisible(node: ITreeNodeStatic, nodeMap: Map<string | number, ITreeNodeStatic>): boolean {
|
|
191
|
+
if (node.parentId === null) return true
|
|
192
|
+
|
|
193
|
+
let currentId: string | number | null = node.parentId
|
|
194
|
+
while (currentId !== null) {
|
|
195
|
+
const parent = nodeMap.get(currentId)
|
|
196
|
+
if (!parent) return false
|
|
197
|
+
|
|
198
|
+
const parentExpanded = this.expandedByDefault
|
|
199
|
+
? !this.expandedNodes.value.has(currentId)
|
|
200
|
+
: this.expandedNodes.value.has(currentId)
|
|
201
|
+
|
|
202
|
+
if (!parentExpanded) return false
|
|
203
|
+
currentId = parent.parentId
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private paginateNodes(visible: ITreeNode[]): ITreeNode[] {
|
|
209
|
+
const pageSize = this.pageSize.value
|
|
210
|
+
const currentPage = this.currentPage.value
|
|
211
|
+
|
|
212
|
+
// pageSize <= 0 means no pagination
|
|
213
|
+
if (pageSize <= 0) {
|
|
214
|
+
this.rowCount.value = visible.length
|
|
215
|
+
return visible
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this.paginateBy === 'root') {
|
|
219
|
+
const rootNodes = visible.filter(n => n.parentId === null)
|
|
220
|
+
const startIndex = (currentPage - 1) * pageSize
|
|
221
|
+
const endIndex = startIndex + pageSize
|
|
222
|
+
const paginatedRoots = rootNodes.slice(startIndex, endIndex)
|
|
223
|
+
const paginatedRootIds = new Set(paginatedRoots.map(n => n.id))
|
|
224
|
+
|
|
225
|
+
const nodeMap = new Map(visible.map(n => [n.id, n]))
|
|
226
|
+
const getRootId = (node: ITreeNode): string | number => {
|
|
227
|
+
let current = node
|
|
228
|
+
while (current.parentId !== null) {
|
|
229
|
+
const parent = nodeMap.get(current.parentId as string | number)
|
|
230
|
+
if (!parent) break
|
|
231
|
+
current = parent
|
|
232
|
+
}
|
|
233
|
+
return current.id
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = visible.filter(node => paginatedRootIds.has(getRootId(node)))
|
|
237
|
+
this.rowCount.value = rootNodes.length
|
|
238
|
+
return result
|
|
239
|
+
} else {
|
|
240
|
+
const startIndex = (currentPage - 1) * pageSize
|
|
241
|
+
const endIndex = startIndex + pageSize
|
|
242
|
+
this.rowCount.value = visible.length
|
|
243
|
+
return visible.slice(startIndex, endIndex)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Override loadPageData
|
|
248
|
+
override async loadPageData(): Promise<void> {
|
|
249
|
+
this.updatePageData()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Public API
|
|
253
|
+
get treeNodes(): ITreeNode[] {
|
|
254
|
+
// Build full tree nodes with expansion state
|
|
255
|
+
return this._staticNodes.value.map(node => ({
|
|
256
|
+
...node,
|
|
257
|
+
isExpanded: this.expandedByDefault
|
|
258
|
+
? !this.expandedNodes.value.has(node.id)
|
|
259
|
+
: this.expandedNodes.value.has(node.id)
|
|
260
|
+
}))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get visibleRows(): ITreeNode[] {
|
|
264
|
+
// Rebuild visible list
|
|
265
|
+
const nodeMap = this._nodeMap.value
|
|
266
|
+
const sortedIds = this._sortedNodeIds.value
|
|
267
|
+
const visibleNodes: ITreeNode[] = []
|
|
268
|
+
|
|
269
|
+
for (const id of sortedIds) {
|
|
270
|
+
const staticNode = nodeMap.get(id)
|
|
271
|
+
if (!staticNode) continue
|
|
272
|
+
|
|
273
|
+
if (this.isNodeVisible(staticNode, nodeMap)) {
|
|
274
|
+
visibleNodes.push({
|
|
275
|
+
...staticNode,
|
|
276
|
+
isExpanded: this.expandedByDefault
|
|
277
|
+
? !this.expandedNodes.value.has(id)
|
|
278
|
+
: this.expandedNodes.value.has(id)
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return this.paginateNodes(visibleNodes)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
toggleNode(nodeId: string | number): void {
|
|
287
|
+
if (this.expandedNodes.value.has(nodeId)) {
|
|
288
|
+
this.expandedNodes.value.delete(nodeId)
|
|
289
|
+
} else {
|
|
290
|
+
this.expandedNodes.value.add(nodeId)
|
|
291
|
+
}
|
|
292
|
+
// Trigger reactivity
|
|
293
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
expand(nodeId: string | number): void {
|
|
297
|
+
this.expandedNodes.value.add(nodeId)
|
|
298
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
collapse(nodeId: string | number): void {
|
|
302
|
+
this.expandedNodes.value.delete(nodeId)
|
|
303
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
expandAll(): void {
|
|
307
|
+
const allIds = this._staticNodes.value.filter(n => n.hasChildren).map(n => n.id)
|
|
308
|
+
this.expandedNodes.value = new Set(allIds)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
collapseAll(): void {
|
|
312
|
+
this.expandedNodes.value = new Set()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getNode(nodeId: string | number): ITreeNode | undefined {
|
|
316
|
+
const staticNode = this._nodeMap.value.get(nodeId)
|
|
317
|
+
if (!staticNode) return undefined
|
|
318
|
+
return {
|
|
319
|
+
...staticNode,
|
|
320
|
+
isExpanded: this.expandedByDefault
|
|
321
|
+
? !this.expandedNodes.value.has(nodeId)
|
|
322
|
+
: this.expandedNodes.value.has(nodeId)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getChildren(nodeId: string | number): ITreeNode[] {
|
|
327
|
+
return this.treeNodes.filter(n => n.parentId === nodeId)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getParent(nodeId: string | number): ITreeNode | undefined {
|
|
331
|
+
const node = this.getNode(nodeId)
|
|
332
|
+
if (!node || node.parentId === null) return undefined
|
|
333
|
+
return this.getNode(node.parentId)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ref, watch, type Ref } from 'vue'
|
|
2
|
+
import { ADataProvider } from './ADataProvider'
|
|
3
|
+
import type {
|
|
4
|
+
IDataResult,
|
|
5
|
+
IDataFilter,
|
|
6
|
+
IDataSort,
|
|
7
|
+
ITreeDataProviderOptions,
|
|
8
|
+
TreePaginationMode
|
|
9
|
+
} from '../types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Tree-specific page data handler that includes tree parameters
|
|
13
|
+
* Server handles tree building, visibility, and pagination
|
|
14
|
+
*/
|
|
15
|
+
export type TTreePageDataHandler = (
|
|
16
|
+
currentPage: number,
|
|
17
|
+
pageSize: number,
|
|
18
|
+
filter: IDataFilter,
|
|
19
|
+
sortList: IDataSort[],
|
|
20
|
+
treeParams: {
|
|
21
|
+
expandedNodes: (string | number)[],
|
|
22
|
+
parentKey: string,
|
|
23
|
+
idKey: string,
|
|
24
|
+
paginateBy: TreePaginationMode,
|
|
25
|
+
expandedByDefault: boolean
|
|
26
|
+
},
|
|
27
|
+
options?: { disableCache?: boolean }
|
|
28
|
+
) => IDataResult | Promise<IDataResult>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Server-side Tree Data Provider
|
|
32
|
+
*
|
|
33
|
+
* Key difference from CTreeClientDataProvider:
|
|
34
|
+
* - Tree structure, visibility, and pagination are handled SERVER-SIDE
|
|
35
|
+
* - Client only sends expanded state to server
|
|
36
|
+
* - Server returns already-paginated visible rows with __treeNode metadata
|
|
37
|
+
* - No allData - server handles everything
|
|
38
|
+
*/
|
|
39
|
+
export class CTreeServerDataProvider extends ADataProvider {
|
|
40
|
+
// Tree configuration (sent to server)
|
|
41
|
+
parentKey: string = 'parentId'
|
|
42
|
+
idKey: string = 'id'
|
|
43
|
+
expandedByDefault: boolean = false
|
|
44
|
+
paginateBy: TreePaginationMode = 'all'
|
|
45
|
+
|
|
46
|
+
// Tree state - toggled nodes (sent to server for visibility calculation)
|
|
47
|
+
expandedNodes: Ref<Set<string | number>> = ref(new Set())
|
|
48
|
+
|
|
49
|
+
// Server-side data handler
|
|
50
|
+
pageDataHandler: Ref<TTreePageDataHandler | null> = ref(null)
|
|
51
|
+
contextKey: Ref<string> = ref("")
|
|
52
|
+
initialLoad: Ref<boolean> = ref(true)
|
|
53
|
+
|
|
54
|
+
constructor(options?: ITreeDataProviderOptions) {
|
|
55
|
+
super(options)
|
|
56
|
+
if (options?.parentKey) this.parentKey = options.parentKey
|
|
57
|
+
if (options?.idKey) this.idKey = options.idKey
|
|
58
|
+
if (options?.expandedByDefault) this.expandedByDefault = options.expandedByDefault
|
|
59
|
+
if (options?.paginateBy) this.paginateBy = options.paginateBy
|
|
60
|
+
|
|
61
|
+
// Watch for changes that require server refetch
|
|
62
|
+
watch([this.expandedNodes, this.currentPage, this.pageSize], () => {
|
|
63
|
+
if (this.pageDataHandler.value && !this.initialLoad.value) {
|
|
64
|
+
this.loadPageData()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setContextKey(key: string) {
|
|
70
|
+
this.contextKey.value = key
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async setPageDataHandler(handler: TTreePageDataHandler) {
|
|
74
|
+
this.pageDataHandler.value = handler
|
|
75
|
+
await this.loadPageData()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load page data from server
|
|
80
|
+
* Server handles tree building, visibility, and pagination
|
|
81
|
+
*/
|
|
82
|
+
async loadPageData(options?: { disableCache?: boolean }): Promise<void> {
|
|
83
|
+
if (!this.pageDataHandler.value || (!this.SSR.value && import.meta.server)) return;
|
|
84
|
+
|
|
85
|
+
const expandedArray = Array.from(this.expandedNodes.value)
|
|
86
|
+
const stableStringify = (val: any) => JSON.stringify(val === undefined ? "" : val)
|
|
87
|
+
const key = [
|
|
88
|
+
this.contextKey.value,
|
|
89
|
+
this.currentPage.value,
|
|
90
|
+
this.pageSize.value,
|
|
91
|
+
stableStringify(this.filter.value),
|
|
92
|
+
stableStringify(this.sortList.value),
|
|
93
|
+
stableStringify(expandedArray)
|
|
94
|
+
].join('-')
|
|
95
|
+
|
|
96
|
+
const treeParams = {
|
|
97
|
+
expandedNodes: expandedArray,
|
|
98
|
+
parentKey: this.parentKey,
|
|
99
|
+
idKey: this.idKey,
|
|
100
|
+
paginateBy: this.paginateBy,
|
|
101
|
+
expandedByDefault: this.expandedByDefault
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const fetcher = async () => await this.pageDataHandler.value!(
|
|
105
|
+
this.currentPage.value,
|
|
106
|
+
this.pageSize.value,
|
|
107
|
+
this.filter.value as IDataFilter,
|
|
108
|
+
this.sortList.value as IDataSort[],
|
|
109
|
+
treeParams,
|
|
110
|
+
options
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// Check for existing data from SSR payload first
|
|
114
|
+
// Skip if disableCache is true
|
|
115
|
+
if (!options?.disableCache && this.SSR.value && this.contextKey.value && this.initialLoad.value) {
|
|
116
|
+
const { data } = useNuxtData(key)
|
|
117
|
+
if (data.value) {
|
|
118
|
+
const result = data.value as IDataResult
|
|
119
|
+
this.pageData.value = result.rows
|
|
120
|
+
this.rowCount.value = result.rowCount
|
|
121
|
+
this.initialLoad.value = false
|
|
122
|
+
this.loading.value = false
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// SSR: use useAsyncData
|
|
128
|
+
if (this.SSR.value && this.contextKey.value) {
|
|
129
|
+
this.loading.value = true
|
|
130
|
+
|
|
131
|
+
// If cache is disabled, we bypass useAsyncData's automatic caching if possible,
|
|
132
|
+
// but useAsyncData is primary for hydration.
|
|
133
|
+
// For specific disableCache request in SSR mode, we might want to just fetch directly
|
|
134
|
+
// OR rely on the fact that the underlying handler will disable cache.
|
|
135
|
+
// However, useAsyncData itself caches based on key.
|
|
136
|
+
// If disableCache is true, we should probably append a random key or manage it otherwise?
|
|
137
|
+
// Actually, if disableCache is true, we likely shouldn't use useAsyncData at all if we want to force refresh?
|
|
138
|
+
// But sticking to standard pattern:
|
|
139
|
+
const { data } = await useAsyncData(key, fetcher)
|
|
140
|
+
if (data.value) {
|
|
141
|
+
const result = data.value as IDataResult
|
|
142
|
+
this.pageData.value = result.rows
|
|
143
|
+
this.rowCount.value = result.rowCount
|
|
144
|
+
this.initialLoad.value = false
|
|
145
|
+
this.loading.value = false
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Non-SSR client-side
|
|
151
|
+
if (!this.SSR.value) {
|
|
152
|
+
onNuxtReady(() => {
|
|
153
|
+
this.loading.value = true
|
|
154
|
+
fetcher().then((returnedData) => {
|
|
155
|
+
this.pageData.value = returnedData.rows
|
|
156
|
+
this.rowCount.value = returnedData.rowCount
|
|
157
|
+
this.initialLoad.value = false
|
|
158
|
+
this.loading.value = false
|
|
159
|
+
}).catch(() => {
|
|
160
|
+
this.initialLoad.value = false
|
|
161
|
+
this.loading.value = false
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fallback
|
|
168
|
+
this.loading.value = true
|
|
169
|
+
const returnedData: IDataResult = await fetcher()
|
|
170
|
+
this.pageData.value = returnedData.rows
|
|
171
|
+
this.rowCount.value = returnedData.rowCount
|
|
172
|
+
this.initialLoad.value = false
|
|
173
|
+
this.loading.value = false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================
|
|
177
|
+
// Expand/Collapse API
|
|
178
|
+
// ============================================
|
|
179
|
+
|
|
180
|
+
toggleNode(nodeId: string | number): void {
|
|
181
|
+
if (this.expandedNodes.value.has(nodeId)) {
|
|
182
|
+
this.expandedNodes.value.delete(nodeId)
|
|
183
|
+
} else {
|
|
184
|
+
this.expandedNodes.value.add(nodeId)
|
|
185
|
+
}
|
|
186
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expand(nodeId: string | number): void {
|
|
190
|
+
this.expandedNodes.value.add(nodeId)
|
|
191
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
collapse(nodeId: string | number): void {
|
|
195
|
+
this.expandedNodes.value.delete(nodeId)
|
|
196
|
+
this.expandedNodes.value = new Set(this.expandedNodes.value)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
collapseAll(): void {
|
|
200
|
+
this.expandedNodes.value = new Set()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
isExpanded(nodeId: string | number): boolean {
|
|
204
|
+
const isToggled = this.expandedNodes.value.has(nodeId)
|
|
205
|
+
return this.expandedByDefault ? !isToggled : isToggled
|
|
206
|
+
}
|
|
207
|
+
}
|