@stellar-expert/ui-framework 1.16.8 → 1.17.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/CLAUDE.md +35 -0
- package/README.md +1125 -3
- package/account/account-address.js +127 -127
- package/account/available-balance.js +22 -22
- package/account/identicon.js +90 -90
- package/account/signer-key.js +65 -64
- package/api/explorer-api-hooks.js +209 -202
- package/api/explorer-api-paginated-list-hooks.js +441 -440
- package/api/explorer-batch-info-loader.js +111 -85
- package/api/explorer-tx-api.js +28 -28
- package/api/ledger-stream.js +15 -0
- package/asset/amount.js +56 -56
- package/asset/asset-icon.js +41 -41
- package/asset/asset-issuer.js +21 -21
- package/asset/asset-link.js +107 -107
- package/asset/asset-list-hooks.js +59 -59
- package/asset/asset-meta-hooks.js +88 -88
- package/asset/asset-selector.js +72 -71
- package/claimable-balance/claimable-balance-claimants.js +23 -18
- package/contract/contract-api.js +15 -0
- package/contract/invocation-info-view.js +10 -2
- package/contract/sc-val.js +131 -107
- package/controls/button-group.js +25 -19
- package/controls/button.js +93 -78
- package/controls/code-block.js +42 -34
- package/controls/dropdown.js +318 -287
- package/controls/external-link.js +10 -4
- package/controls/info-tooltip.js +23 -16
- package/controls/slider.js +29 -19
- package/controls/tabs.js +94 -94
- package/controls/tooltip.js +244 -240
- package/controls/update-highlighter.js +32 -27
- package/date/date-selector.js +56 -54
- package/date/elapsed-time.js +28 -21
- package/dex/price-dynamic.js +53 -44
- package/directory/directory-hooks.js +109 -97
- package/effect/effect-description.js +346 -344
- package/errors/error-boundary.js +110 -97
- package/horizon/horizon-account-helpers.js +104 -104
- package/horizon/horizon-ledger-helpers.js +35 -35
- package/horizon/horizon-trades-helper.js +88 -88
- package/horizon/horizon-transaction-helpers.js +36 -36
- package/index.d.ts +1241 -0
- package/interaction/accordion.js +43 -35
- package/interaction/autofocus.js +13 -9
- package/interaction/block-select.js +64 -53
- package/interaction/copy-to-clipboard.js +25 -18
- package/interaction/inline-progress.js +20 -15
- package/interaction/qr-code.js +34 -34
- package/interaction/responsive.js +20 -20
- package/interaction/spoiler.js +52 -39
- package/interaction/theme-selector.js +13 -10
- package/ledger/ledger-entry-href-formatter.js +27 -26
- package/ledger/ledger-entry-link.js +93 -58
- package/ledger/ledger-info-parser.js +46 -18
- package/meta/page-meta-tags.js +243 -238
- package/module/dynamic-module.js +47 -47
- package/package.json +41 -40
- package/state/on-screen-hooks.js +22 -22
- package/state/page-visibility-helpers.js +29 -16
- package/state/page-visibility-hooks.js +13 -11
- package/state/screen-orientation-hooks.js +19 -15
- package/state/state-hooks.js +76 -76
- package/state/stellar-network-hooks.js +56 -44
- package/state/theme.js +29 -28
- package/stellar/key-type.js +92 -91
- package/stellar/ledger-generic-id.js +39 -39
- package/stellar/signature-hint-utils.js +65 -65
- package/toast/toast-notifications-block.js +59 -59
- package/tx/op-description-view.js +945 -942
- package/tx/op-icon.js +98 -98
- package/tx/parser/op-balance-changes.js +66 -66
- package/tx/parser/op-descriptor.js +51 -48
- package/tx/parser/tx-details-parser.js +81 -81
- package/tx/parser/tx-matcher.js +371 -371
- package/tx/parser/type-filter-matcher.js +126 -126
- package/tx/tx-list-hooks.js +32 -18
- package/tx/tx-operations-list.js +117 -116
|
@@ -1,440 +1,441 @@
|
|
|
1
|
-
import {useRef} from 'react'
|
|
2
|
-
import isEqual from 'react-fast-compare'
|
|
3
|
-
import {parseQuery, stringifyQuery, navigation} from '@stellar-expert/navigation'
|
|
4
|
-
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
5
|
-
import {useDependantState} from '../state/state-hooks'
|
|
6
|
-
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
|
-
import apiCache from './api-cache'
|
|
8
|
-
|
|
9
|
-
function inverseOrder(order) {
|
|
10
|
-
return order === 'desc' ? 'asc' : 'desc'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class PaginatedListViewModel {
|
|
14
|
-
/**
|
|
15
|
-
* Create new instance of PaginatedListViewModel
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {Object} [props] - Extra model params
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {'asc'|'desc'} [props.defaultSortOrder] - Results sorting order
|
|
23
|
-
* @param {Object} [props.defaultQueryParams] - Default query values - query params not set if default
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {
|
|
26
|
-
*/
|
|
27
|
-
constructor(endpoint, props = {limit: 20}) {
|
|
28
|
-
this.endpoint = endpoint
|
|
29
|
-
this.data = []
|
|
30
|
-
this.defaultQueryParams = {}
|
|
31
|
-
this.limit = props.limit
|
|
32
|
-
this.ttl = props.ttl
|
|
33
|
-
this.query = props.query
|
|
34
|
-
this.dataProcessingCallback = props.dataProcessingCallback
|
|
35
|
-
this.defaultSortOrder = props.defaultSortOrder
|
|
36
|
-
this.updateLocation = props.updateLocation
|
|
37
|
-
if (props.autoReverseRecordsOrder !== undefined) {
|
|
38
|
-
this.autoReverseRecordsOrder = props.autoReverseRecordsOrder
|
|
39
|
-
}
|
|
40
|
-
if (props.autoLoadLastPage !== undefined) {
|
|
41
|
-
this.autoLoadLastPage = props.autoLoadLastPage
|
|
42
|
-
}
|
|
43
|
-
//reconstruct state from query
|
|
44
|
-
if (navigation.query.cursor) {
|
|
45
|
-
const {cursor, sort, order} = navigation.query
|
|
46
|
-
this.nextCursor = stringifyQuery({cursor, sort, order})
|
|
47
|
-
}
|
|
48
|
-
if (props.defaultQueryParams) {
|
|
49
|
-
this.defaultQueryParams = props.defaultQueryParams
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* API endpoint
|
|
55
|
-
* @type {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* @
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
* @
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
*
|
|
181
|
-
* @
|
|
182
|
-
* @
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
*
|
|
197
|
-
* @
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
this.
|
|
202
|
-
this.
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
*
|
|
224
|
-
* @param {
|
|
225
|
-
* @param {{}}
|
|
226
|
-
* @
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.
|
|
233
|
-
this.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.
|
|
251
|
-
this.
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
this.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*
|
|
267
|
-
* @param {
|
|
268
|
-
* @param {
|
|
269
|
-
* @
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
this.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
*
|
|
303
|
-
* @
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
*
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
this.
|
|
348
|
-
this.
|
|
349
|
-
this.
|
|
350
|
-
this.
|
|
351
|
-
this.
|
|
352
|
-
this.
|
|
353
|
-
this.
|
|
354
|
-
this.
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* @
|
|
362
|
-
* @property {
|
|
363
|
-
* @property {
|
|
364
|
-
* @property {
|
|
365
|
-
* @property {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
*
|
|
371
|
-
* @param {
|
|
372
|
-
* @param {
|
|
373
|
-
* @param {
|
|
374
|
-
* @param {
|
|
375
|
-
* @param {
|
|
376
|
-
* @param {
|
|
377
|
-
* @param {
|
|
378
|
-
* @param {
|
|
379
|
-
* @param {
|
|
380
|
-
* @param {
|
|
381
|
-
* @param {
|
|
382
|
-
* @
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1
|
+
import {useRef} from 'react'
|
|
2
|
+
import isEqual from 'react-fast-compare'
|
|
3
|
+
import {parseQuery, stringifyQuery, navigation} from '@stellar-expert/navigation'
|
|
4
|
+
import {useStellarNetwork} from '../state/stellar-network-hooks'
|
|
5
|
+
import {useDependantState} from '../state/state-hooks'
|
|
6
|
+
import {fetchExplorerApi} from './explorer-api-call'
|
|
7
|
+
import apiCache from './api-cache'
|
|
8
|
+
|
|
9
|
+
function inverseOrder(order) {
|
|
10
|
+
return order === 'desc' ? 'asc' : 'desc'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class PaginatedListViewModel {
|
|
14
|
+
/**
|
|
15
|
+
* Create new instance of PaginatedListViewModel
|
|
16
|
+
* @param {string} endpoint - API endpoint
|
|
17
|
+
* @param {Object} [props] - Extra model params
|
|
18
|
+
* @param {number} [props.ttl] - Cache time-to-live period
|
|
19
|
+
* @param {number} [props.limit] - Rows limit
|
|
20
|
+
* @param {boolean} [props.autoReverseRecordsOrder] - Reverse order to match default grid order
|
|
21
|
+
* @param {boolean} [props.autoLoadLastPage] - Whether to load last page flag
|
|
22
|
+
* @param {'asc'|'desc'} [props.defaultSortOrder] - Results sorting order
|
|
23
|
+
* @param {Object} [props.defaultQueryParams] - Default query values - query params not set if default
|
|
24
|
+
* @param {function} [props.dataProcessingCallback] - Callback called for the fetched data
|
|
25
|
+
* @param {boolean|function} [props.updateLocation] - Whether to update browser location
|
|
26
|
+
*/
|
|
27
|
+
constructor(endpoint, props = {limit: 20}) {
|
|
28
|
+
this.endpoint = endpoint
|
|
29
|
+
this.data = []
|
|
30
|
+
this.defaultQueryParams = {}
|
|
31
|
+
this.limit = props.limit
|
|
32
|
+
this.ttl = props.ttl
|
|
33
|
+
this.query = props.query
|
|
34
|
+
this.dataProcessingCallback = props.dataProcessingCallback
|
|
35
|
+
this.defaultSortOrder = props.defaultSortOrder
|
|
36
|
+
this.updateLocation = props.updateLocation
|
|
37
|
+
if (props.autoReverseRecordsOrder !== undefined) {
|
|
38
|
+
this.autoReverseRecordsOrder = props.autoReverseRecordsOrder
|
|
39
|
+
}
|
|
40
|
+
if (props.autoLoadLastPage !== undefined) {
|
|
41
|
+
this.autoLoadLastPage = props.autoLoadLastPage
|
|
42
|
+
}
|
|
43
|
+
//reconstruct state from query
|
|
44
|
+
if (navigation.query.cursor) {
|
|
45
|
+
const {cursor, sort, order} = navigation.query
|
|
46
|
+
this.nextCursor = stringifyQuery({cursor, sort, order})
|
|
47
|
+
}
|
|
48
|
+
if (props.defaultQueryParams) {
|
|
49
|
+
this.defaultQueryParams = props.defaultQueryParams
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* API endpoint
|
|
55
|
+
* @type {string}
|
|
56
|
+
* @readonly
|
|
57
|
+
*/
|
|
58
|
+
endpoint = ''
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Batch size
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
limit = 20
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Time-to-live cache period (in seconds)
|
|
68
|
+
* @type {number}
|
|
69
|
+
*/
|
|
70
|
+
ttl = 30
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
*
|
|
74
|
+
* @type {function|Object}
|
|
75
|
+
*/
|
|
76
|
+
query = null
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Automatically reverse records order
|
|
80
|
+
* @type {boolean}
|
|
81
|
+
*/
|
|
82
|
+
autoReverseRecordsOrder = false
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sorting order
|
|
86
|
+
* @type {string}
|
|
87
|
+
*/
|
|
88
|
+
defaultSortOrder = 'desc'
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load last meaningful page if now results returned from the server
|
|
92
|
+
* @type {boolean}
|
|
93
|
+
*/
|
|
94
|
+
autoLoadLastPage = true
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Function used to process data retrieved from the server
|
|
98
|
+
* @type {function}
|
|
99
|
+
*/
|
|
100
|
+
dataProcessingCallback = null
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @type {function}
|
|
104
|
+
*/
|
|
105
|
+
onError = null
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Data received from server
|
|
109
|
+
* @type {Object}
|
|
110
|
+
*/
|
|
111
|
+
data
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Response loaded flag
|
|
115
|
+
* @type {boolean}
|
|
116
|
+
*/
|
|
117
|
+
loaded = false
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Fetch-in-progress flag
|
|
121
|
+
* @type {boolean}
|
|
122
|
+
*/
|
|
123
|
+
loading = false
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether the next page is available
|
|
127
|
+
* @type {boolean}
|
|
128
|
+
*/
|
|
129
|
+
canLoadNextPage = false
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether the prev page is available
|
|
133
|
+
* @type {boolean}
|
|
134
|
+
*/
|
|
135
|
+
canLoadPrevPage = false
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Next page cursor
|
|
139
|
+
* @type {string}
|
|
140
|
+
*/
|
|
141
|
+
nextCursor
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Previous page cursor
|
|
145
|
+
* @type {string}
|
|
146
|
+
*/
|
|
147
|
+
prevCursor
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @private
|
|
151
|
+
* @type {function}
|
|
152
|
+
*/
|
|
153
|
+
updateApiResponseData = null
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Load portion of data from the server
|
|
157
|
+
* @param {1|-1} page
|
|
158
|
+
* @return {Promise<ExplorerApiListResponse>}
|
|
159
|
+
*/
|
|
160
|
+
load(page) {
|
|
161
|
+
const paginationParams = {skip: undefined},
|
|
162
|
+
navCursor = page < 0 ? this.prevCursor : this.nextCursor
|
|
163
|
+
|
|
164
|
+
if (navCursor) {
|
|
165
|
+
parseQuery(navCursor.split('?')[1] || '', paginationParams)
|
|
166
|
+
} else {
|
|
167
|
+
this.nextCursor = stringifyQuery({
|
|
168
|
+
cursor: navigation.query.cursor,
|
|
169
|
+
sort: navigation.query.sort,
|
|
170
|
+
order: navigation.query.order
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
//const externalQueryParams = typeof this.query === 'function' ? this.query() : this.query
|
|
174
|
+
//prepare query params
|
|
175
|
+
const queryParams = Object.assign({}, this.defaultQueryParams, this.query, paginationParams, {limit: this.limit})
|
|
176
|
+
return this.loadPage(queryParams)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Reverse order and load the page
|
|
181
|
+
* @param queryParams
|
|
182
|
+
* @return {Promise<ExplorerApiListResponse>}
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
async loadLastPage(queryParams) {
|
|
186
|
+
const {order} = queryParams,
|
|
187
|
+
overrides = {
|
|
188
|
+
order: inverseOrder(order),
|
|
189
|
+
cursor: undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.loadPage(Object.assign({}, queryParams, overrides))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Load data page from the server
|
|
197
|
+
* @param {{}} queryParams
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
async loadPage(queryParams) {
|
|
201
|
+
this.loaded = false
|
|
202
|
+
this.loading = true
|
|
203
|
+
this.updateQuery(queryParams)
|
|
204
|
+
this.updateApiResponseData(this.toJSON())
|
|
205
|
+
const [path] = this.endpoint.split('?')
|
|
206
|
+
const endpointWithQuery = path + stringifyQuery(queryParams)
|
|
207
|
+
|
|
208
|
+
//try to retrieve data from the browser cache
|
|
209
|
+
const fromCache = this.ttl && apiCache.get(endpointWithQuery)
|
|
210
|
+
//fetch from the server only if there is no data or it is expired
|
|
211
|
+
if (!fromCache || fromCache.isExpired) {
|
|
212
|
+
const data = await fetchExplorerApi(endpointWithQuery)
|
|
213
|
+
return this.processResponseData(endpointWithQuery, data, queryParams)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (fromCache && !fromCache.isStale) {
|
|
217
|
+
//if the cached data is up to date - proceed with it
|
|
218
|
+
return this.processResponseData(endpointWithQuery, fromCache.data, queryParams)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Retrieve and convert data from the API response
|
|
224
|
+
* @param {string} endpointWithQuery
|
|
225
|
+
* @param {{}} data
|
|
226
|
+
* @param {{}} queryParams
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
processResponseData(endpointWithQuery, data, queryParams) {
|
|
230
|
+
if (data.error) {
|
|
231
|
+
console.error(data.error)
|
|
232
|
+
this.loaded = true
|
|
233
|
+
this.loading = false
|
|
234
|
+
this.error = data
|
|
235
|
+
} else {
|
|
236
|
+
const {_links, _embedded} = data
|
|
237
|
+
let records = _embedded.records.slice()
|
|
238
|
+
//we reached the end of the query
|
|
239
|
+
if (!records.length && this.autoLoadLastPage && this.data && this.data.length) {
|
|
240
|
+
//load first/last meaningful page
|
|
241
|
+
setTimeout(() => this.loadLastPage(queryParams), 500)
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
if (this.autoReverseRecordsOrder && _links.self.href.includes('order=' + inverseOrder(this.defaultSortOrder))) {
|
|
245
|
+
records.reverse()
|
|
246
|
+
}
|
|
247
|
+
if (this.dataProcessingCallback) {
|
|
248
|
+
records = this.dataProcessingCallback(records)
|
|
249
|
+
}
|
|
250
|
+
this.data = records
|
|
251
|
+
this.loaded = true
|
|
252
|
+
this.loading = false
|
|
253
|
+
this.updateNav(_links)
|
|
254
|
+
}
|
|
255
|
+
//in case of error set small ttl in order to try re-fetching in 4 seconds
|
|
256
|
+
if (this.ttl >= 0) {
|
|
257
|
+
apiCache.set(endpointWithQuery, data, data.error ? 4 : this.ttl)
|
|
258
|
+
}
|
|
259
|
+
//update response data
|
|
260
|
+
const res = this.toJSON()
|
|
261
|
+
this.updateApiResponseData(res)
|
|
262
|
+
return res
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Update navigation links retrieved from the response
|
|
267
|
+
* @param {string} self
|
|
268
|
+
* @param {string} next
|
|
269
|
+
* @param {string} prev
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
updateNav({self, next, prev}) {
|
|
273
|
+
this.canLoadNextPage = true //this.nextCursor && this.nextCursor !== self.href && records.length >= this.limit
|
|
274
|
+
this.canLoadPrevPage = true //this.prevCursor && this.prevCursor !== self.href
|
|
275
|
+
const selfQuery = parseQuery(self.href.split('?')[1])
|
|
276
|
+
if ((selfQuery.order === inverseOrder(this.defaultSortOrder))) {
|
|
277
|
+
this.prevCursor = next ? next.href : null
|
|
278
|
+
this.nextCursor = prev ? prev.href : null
|
|
279
|
+
if (!selfQuery.cursor) {
|
|
280
|
+
this.canLoadNextPage = false
|
|
281
|
+
}
|
|
282
|
+
if (this.data.length < this.limit) {
|
|
283
|
+
this.canLoadPrevPage = false
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
this.nextCursor = next ? next.href : null
|
|
287
|
+
this.prevCursor = prev ? prev.href : null
|
|
288
|
+
if (!selfQuery.cursor) {
|
|
289
|
+
this.canLoadPrevPage = false
|
|
290
|
+
}
|
|
291
|
+
if (this.data.length < this.limit) {
|
|
292
|
+
this.canLoadNextPage = false
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (selfQuery.cursor === '0') {
|
|
297
|
+
this.canLoadPrevPage = false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Update location query string
|
|
303
|
+
* @param {{}} queryParams
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
updateQuery(queryParams) {
|
|
307
|
+
const {updateLocation} = this
|
|
308
|
+
if (!updateLocation)
|
|
309
|
+
return
|
|
310
|
+
let paramsToSet = {}
|
|
311
|
+
for (let key in queryParams)
|
|
312
|
+
if (queryParams.hasOwnProperty(key)) {
|
|
313
|
+
if (key === 'limit') continue
|
|
314
|
+
let value = queryParams[key]
|
|
315
|
+
//ignore default params
|
|
316
|
+
if (this.defaultQueryParams[key] === value) {
|
|
317
|
+
value = undefined
|
|
318
|
+
}
|
|
319
|
+
paramsToSet[key] = value
|
|
320
|
+
}
|
|
321
|
+
if (typeof updateLocation === 'function') {
|
|
322
|
+
paramsToSet = updateLocation(paramsToSet)
|
|
323
|
+
}
|
|
324
|
+
navigation.updateQuery(paramsToSet)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Convert model to a plain object representation
|
|
329
|
+
* @return {{loaded: boolean, data: ({}[]), load: function, reset: function loading: boolean, canLoadNextPage: boolean, canLoadPrevPage: boolean}}
|
|
330
|
+
*/
|
|
331
|
+
toJSON() {
|
|
332
|
+
return {
|
|
333
|
+
data: this.data || [],
|
|
334
|
+
loaded: this.loaded,
|
|
335
|
+
loading: this.loading,
|
|
336
|
+
canLoadPrevPage: this.canLoadPrevPage,
|
|
337
|
+
canLoadNextPage: this.canLoadNextPage,
|
|
338
|
+
load: this.load.bind(this),
|
|
339
|
+
reset: this.reset.bind(this)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Reset model to the initial state
|
|
345
|
+
*/
|
|
346
|
+
reset() {
|
|
347
|
+
this.data = []
|
|
348
|
+
this.loaded = false
|
|
349
|
+
this.loading = false
|
|
350
|
+
this.canLoadNextPage = false
|
|
351
|
+
this.canLoadPrevPage = false
|
|
352
|
+
this.nextCursor = undefined
|
|
353
|
+
this.prevCursor = undefined
|
|
354
|
+
this.currentQueryParams = undefined
|
|
355
|
+
this.updateQuery({cursor: undefined, sort: undefined, order: undefined})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* @typedef {Object} ExplorerApiListResponse
|
|
362
|
+
* @property {Object[]} data - Data retrieved from the server
|
|
363
|
+
* @property {boolean} loaded - Response result loaded flag
|
|
364
|
+
* @property {function} load - Load page function
|
|
365
|
+
* @property {boolean} canLoadPrevPage - Whether the prev page is available
|
|
366
|
+
* @property {boolean} canLoadNextPage - Whether the next page is available
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
*
|
|
371
|
+
* @param {string|APIEndpointParams} apiEndpoint - Server API endpoint to use as a data source.
|
|
372
|
+
* @param {number} [ttl] - Cache time-to-live in seconds.
|
|
373
|
+
* @param {number} [limit] - Rows per batch limit.
|
|
374
|
+
* @param {boolean} [autoReverseRecordsOrder] - Reverse order to match default grid order.
|
|
375
|
+
* @param {'asc'|'desc'} [defaultSortOrder] - Reverse order to match default grid order.
|
|
376
|
+
* @param {boolean} [autoLoadLastPage] - Load last meaningful page if now results returned from the server.
|
|
377
|
+
* @param {boolean} [includeNetwork] - Whether to include network prefix in the endpoint path.
|
|
378
|
+
* @param {function} [dataProcessingCallback] - Callback called for the fetched data.
|
|
379
|
+
* @param {Object} [defaultQueryParams] - Default query values - query params not set if default.
|
|
380
|
+
* @param {boolean} [autoLoad] - Default query values - query params not set if default.
|
|
381
|
+
* @param {boolean} [updateLocation] - Automatically update browser query string on navigation.
|
|
382
|
+
* @param {Array} [dependencies] - Additional dependencies to track for state updates.
|
|
383
|
+
* @return {ExplorerApiListResponse}
|
|
384
|
+
*/
|
|
385
|
+
export function useExplorerPaginatedApi(apiEndpoint,
|
|
386
|
+
{
|
|
387
|
+
ttl = 30,
|
|
388
|
+
limit = 20,
|
|
389
|
+
autoReverseRecordsOrder = false,
|
|
390
|
+
defaultSortOrder = 'desc',
|
|
391
|
+
autoLoadLastPage = true,
|
|
392
|
+
includeNetwork = true,
|
|
393
|
+
defaultQueryParams = {},
|
|
394
|
+
dataProcessingCallback,
|
|
395
|
+
autoLoad = true,
|
|
396
|
+
updateLocation = true
|
|
397
|
+
} = {},
|
|
398
|
+
dependencies = []) {
|
|
399
|
+
if (!apiEndpoint)
|
|
400
|
+
throw new Error(`Invalid API endpoint: ${apiEndpoint}`)
|
|
401
|
+
const network = useStellarNetwork()
|
|
402
|
+
const pinRef = useRef(null)
|
|
403
|
+
if (typeof apiEndpoint === 'string') {
|
|
404
|
+
const [path, query] = apiEndpoint.split('?')
|
|
405
|
+
apiEndpoint = {
|
|
406
|
+
path,
|
|
407
|
+
query: parseQuery(query)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (defaultQueryParams.order) {
|
|
411
|
+
defaultSortOrder = defaultQueryParams.order
|
|
412
|
+
}
|
|
413
|
+
const endpoint = includeNetwork ? `${network}/${apiEndpoint.path}` : apiEndpoint.path
|
|
414
|
+
|
|
415
|
+
const [apiResponseData, updateApiResponseData] = useDependantState(() => {
|
|
416
|
+
const res = new PaginatedListViewModel(endpoint, {
|
|
417
|
+
ttl,
|
|
418
|
+
limit,
|
|
419
|
+
query: apiEndpoint.query,
|
|
420
|
+
dataProcessingCallback,
|
|
421
|
+
autoLoadLastPage,
|
|
422
|
+
autoReverseRecordsOrder,
|
|
423
|
+
defaultSortOrder,
|
|
424
|
+
defaultQueryParams,
|
|
425
|
+
updateLocation
|
|
426
|
+
})
|
|
427
|
+
pinRef.current = res
|
|
428
|
+
if (autoLoad) {
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
res.load()
|
|
431
|
+
}, 100)
|
|
432
|
+
}
|
|
433
|
+
return res.toJSON()
|
|
434
|
+
}, [JSON.stringify(apiEndpoint), limit, ttl, autoReverseRecordsOrder, autoLoadLastPage, autoLoad, ...dependencies])
|
|
435
|
+
|
|
436
|
+
pinRef.current.updateApiResponseData = function (newData) {
|
|
437
|
+
updateApiResponseData(prevListData => isEqual(prevListData, newData) ? prevListData : newData)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return apiResponseData
|
|
441
|
+
}
|