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