@promoboxx/use-filter 1.11.1 → 2.0.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/.github/workflows/main.yml +38 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +185 -0
- package/Makefile +25 -0
- package/eslint.config.js +30 -0
- package/mise.toml +3 -0
- package/package.json +33 -43
- package/prettier.config.js +3 -0
- package/src/lib/buildDefaultFilterInfo.ts +36 -0
- package/src/lib/getOffsetFromPage.ts +5 -0
- package/src/lib/getPageFromOffset.ts +5 -0
- package/src/lib/shallowEqual.test.ts +71 -0
- package/src/lib/shallowEqual.ts +26 -0
- package/src/store/index.ts +30 -0
- package/src/store/localStorageStore.ts +36 -0
- package/src/store/memoryStore.ts +27 -0
- package/src/store/reduxHelpers/createActions.test.ts +32 -0
- package/src/store/reduxHelpers/createActions.ts +56 -0
- package/src/store/reduxHelpers/createReducer.test.ts +65 -0
- package/src/store/reduxHelpers/createReducer.ts +47 -0
- package/src/store/reduxStore.ts +78 -0
- package/src/store/urlParamStore.test.ts +131 -0
- package/src/store/urlParamStore.ts +85 -0
- package/src/useFilter.test.tsx +822 -0
- package/src/useFilter.ts +524 -0
- package/src/useSimpleFilter.test.tsx +676 -0
- package/src/useSimpleFilter.ts +397 -0
- package/src/vitest-env.d.ts +1 -0
- package/tsconfig.json +76 -0
- package/tsdown.config.ts +30 -0
- package/vite.config.ts +9 -0
- package/dist/lib/buildDefaultFilterInfo.d.ts +0 -3
- package/dist/lib/buildDefaultFilterInfo.js +0 -35
- package/dist/lib/getOffsetFromPage.d.ts +0 -2
- package/dist/lib/getOffsetFromPage.js +0 -6
- package/dist/lib/getPageFromOffset.d.ts +0 -2
- package/dist/lib/getPageFromOffset.js +0 -6
- package/dist/lib/shallowEqual.d.ts +0 -2
- package/dist/lib/shallowEqual.js +0 -23
- package/dist/store/index.d.ts +0 -10
- package/dist/store/index.js +0 -16
- package/dist/store/localStorageStore.d.ts +0 -3
- package/dist/store/localStorageStore.js +0 -31
- package/dist/store/memoryStore.d.ts +0 -3
- package/dist/store/memoryStore.js +0 -23
- package/dist/store/reduxHelpers/createActions.d.ts +0 -16
- package/dist/store/reduxHelpers/createActions.js +0 -27
- package/dist/store/reduxHelpers/createReducer.d.ts +0 -8
- package/dist/store/reduxHelpers/createReducer.js +0 -26
- package/dist/store/reduxStore.d.ts +0 -15
- package/dist/store/reduxStore.js +0 -67
- package/dist/store/urlParamStore.d.ts +0 -2
- package/dist/store/urlParamStore.js +0 -86
- package/dist/useFilter.d.ts +0 -103
- package/dist/useFilter.js +0 -254
- package/dist/useSimpleFilter.d.ts +0 -86
- package/dist/useSimpleFilter.js +0 -173
package/src/useFilter.ts
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import buildDefaultFilterInfo from './lib/buildDefaultFilterInfo'
|
|
4
|
+
import getOffsetFromPage from './lib/getOffsetFromPage'
|
|
5
|
+
import getPageFromOffset from './lib/getPageFromOffset'
|
|
6
|
+
import shallowEqual from './lib/shallowEqual'
|
|
7
|
+
import { getFilterStore } from './store'
|
|
8
|
+
import type { FilterStore } from './store'
|
|
9
|
+
|
|
10
|
+
type MaybePromise<T> = T | Promise<T>
|
|
11
|
+
|
|
12
|
+
interface UseFilterOptions<TFilter extends Record<string, unknown>, TResult> {
|
|
13
|
+
/**
|
|
14
|
+
* Default values for your filter. When calling `.reset` your filter will be
|
|
15
|
+
* set to this.
|
|
16
|
+
* Changing these values does not cause a call to happen.
|
|
17
|
+
*/
|
|
18
|
+
defaultFilterInfo?: Partial<FilterInfo<TFilter>>
|
|
19
|
+
/**
|
|
20
|
+
* Enable this if you are not using the data caching of this hook.
|
|
21
|
+
*/
|
|
22
|
+
shouldForceRunOnMount?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Called whenever the filter changes.
|
|
25
|
+
*/
|
|
26
|
+
onChange: (
|
|
27
|
+
filterInfo: FilterInfo<TFilter>,
|
|
28
|
+
reason: UseFilterUpdateReason,
|
|
29
|
+
) => MaybePromise<UseFilterOnChangeResult<TFilter, TResult>>
|
|
30
|
+
/**
|
|
31
|
+
* In case you want to change the default debounce duration.
|
|
32
|
+
*/
|
|
33
|
+
debounceDuration?: number
|
|
34
|
+
onBeforeSaveFilter?: (filterInfo: FilterInfo<TFilter>) => FilterInfo<TFilter>
|
|
35
|
+
|
|
36
|
+
store?: FilterStore
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FilterApi<TFilter extends Record<string, unknown>, TResult> {
|
|
40
|
+
/**
|
|
41
|
+
* Whether the system is debouncing or waiting for your `onChange` to finish.
|
|
42
|
+
*/
|
|
43
|
+
isLoading: boolean
|
|
44
|
+
/**
|
|
45
|
+
* Any cached data for this filter.
|
|
46
|
+
*/
|
|
47
|
+
data: TResult | null | undefined
|
|
48
|
+
/**
|
|
49
|
+
* Access to the filter and its metadata.
|
|
50
|
+
*/
|
|
51
|
+
filterInfo: FilterInfo<TFilter>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether the filter existed in the system when the component mounted.
|
|
55
|
+
* @deprecated
|
|
56
|
+
*/
|
|
57
|
+
doesFilterExist: boolean
|
|
58
|
+
/**
|
|
59
|
+
* Why the last update happened.
|
|
60
|
+
*/
|
|
61
|
+
updateReason: UseFilterUpdateReason
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Update one or more values in your filter.
|
|
65
|
+
*/
|
|
66
|
+
updateFilter: (
|
|
67
|
+
filter: Partial<TFilter>,
|
|
68
|
+
shouldRunImmediately?: boolean,
|
|
69
|
+
) => void
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resets the filter back to `defaultFilterInfo`.
|
|
73
|
+
*/
|
|
74
|
+
resetFilter: (shouldRunImmediately?: boolean) => void
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Changes the offset. Will update `page`.
|
|
78
|
+
*/
|
|
79
|
+
setOffset: (offset: number | string, shouldRunImmediately?: boolean) => void
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Changes the page. Will update `offset`.
|
|
83
|
+
*/
|
|
84
|
+
setPage: (page: number | string, shouldRunImmediately?: boolean) => void
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Changes the page size.
|
|
88
|
+
*/
|
|
89
|
+
setPageSize: (
|
|
90
|
+
pageSize: number | string,
|
|
91
|
+
shouldRunImmediately?: boolean,
|
|
92
|
+
) => void
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Change the sort method.
|
|
96
|
+
*/
|
|
97
|
+
setSort: (sort: string | undefined, shouldRunImmediately?: boolean) => void
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Changes the cursor.
|
|
101
|
+
*/
|
|
102
|
+
setCursor: (
|
|
103
|
+
cursor: string | null | undefined,
|
|
104
|
+
shouldRunImmediately?: boolean,
|
|
105
|
+
) => void
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Forces a refresh of the filter.
|
|
109
|
+
*/
|
|
110
|
+
forceRefresh: (shouldRunImmediately?: boolean) => void
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type UseFilterUpdateReason = 'initial' | 'filter' | 'pagination'
|
|
114
|
+
|
|
115
|
+
function useFilter<TFilter extends Record<string, unknown>, TResult>(
|
|
116
|
+
namespace: string,
|
|
117
|
+
options: UseFilterOptions<TFilter, TResult>,
|
|
118
|
+
) {
|
|
119
|
+
// Having these types of refs helps get around rules of hooks, and they need
|
|
120
|
+
// to be updated each render.
|
|
121
|
+
const ctxRefValue = {
|
|
122
|
+
namespace,
|
|
123
|
+
onChange: options.onChange,
|
|
124
|
+
debounceDuration: options.debounceDuration,
|
|
125
|
+
defaultFilterInfo: options.defaultFilterInfo,
|
|
126
|
+
onBeforeSaveFilter: options.onBeforeSaveFilter,
|
|
127
|
+
store: options.store,
|
|
128
|
+
}
|
|
129
|
+
const ctxRef = useRef(ctxRefValue)
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
ctxRef.current = ctxRefValue
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const [filterInfo, setFilterInfoState] = useState(() => {
|
|
135
|
+
const fromStore = getFilterStore(ctxRef.current.store).getFilter<TFilter>(
|
|
136
|
+
namespace,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// If the filter is already in the store, we have to make sure it ...
|
|
140
|
+
// - has both the filter and filterInfo
|
|
141
|
+
// - has all the fields the user passed to the hook
|
|
142
|
+
if (fromStore) {
|
|
143
|
+
const filterInfo = buildDefaultFilterInfo({
|
|
144
|
+
...fromStore,
|
|
145
|
+
shouldRunImmediately: true,
|
|
146
|
+
filter: {
|
|
147
|
+
...options.defaultFilterInfo?.filter,
|
|
148
|
+
...fromStore.filter,
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return filterInfo
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
const [data, setData] = useState(() =>
|
|
156
|
+
getFilterStore(ctxRef.current.store).getData<TResult>(namespace),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const [isLoading, setIsLoading] = useState(!filterInfo)
|
|
160
|
+
const doesFilterExist = useRef(!!filterInfo)
|
|
161
|
+
const lastRefreshAtRef = useRef(
|
|
162
|
+
// if shouldForceRunOnMount is set, always use -1
|
|
163
|
+
// otherwise, try grabbing lastRefreshAt
|
|
164
|
+
// then, just use -1 cause there's no other option
|
|
165
|
+
options.shouldForceRunOnMount
|
|
166
|
+
? -1
|
|
167
|
+
: filterInfo
|
|
168
|
+
? filterInfo.lastRefreshAt
|
|
169
|
+
: -1,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const updateReasonRef = useRef<UseFilterUpdateReason>('initial')
|
|
173
|
+
|
|
174
|
+
// Call onChange when data changes.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
setIsLoading(true)
|
|
177
|
+
|
|
178
|
+
// If there is no existing filter info, set it to the defaults and return.
|
|
179
|
+
// This same effect will trigger next go.
|
|
180
|
+
if (!filterInfo) {
|
|
181
|
+
setFilterInfoState({
|
|
182
|
+
...buildDefaultFilterInfo(ctxRef.current.defaultFilterInfo),
|
|
183
|
+
shouldRunImmediately: true,
|
|
184
|
+
})
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If the lastRefreshAt hasn't changed, don't make a request.
|
|
189
|
+
if (lastRefreshAtRef.current === filterInfo.lastRefreshAt) {
|
|
190
|
+
setIsLoading(false)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const makeRequest = () => {
|
|
195
|
+
lastRefreshAtRef.current = filterInfo.lastRefreshAt
|
|
196
|
+
const response = ctxRef.current.onChange(
|
|
197
|
+
filterInfo,
|
|
198
|
+
updateReasonRef.current,
|
|
199
|
+
)
|
|
200
|
+
getFilterStore(ctxRef.current.store).saveFilter(
|
|
201
|
+
namespace,
|
|
202
|
+
ctxRef.current.onBeforeSaveFilter
|
|
203
|
+
? ctxRef.current.onBeforeSaveFilter(filterInfo)
|
|
204
|
+
: filterInfo,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if (!response) {
|
|
208
|
+
setIsLoading(false)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleResponse(
|
|
213
|
+
response?: UseFilterOnChangeResult<TFilter, TResult>,
|
|
214
|
+
) {
|
|
215
|
+
if (!response) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (response.filterInfo) {
|
|
220
|
+
setFilterInfoState((previous) => {
|
|
221
|
+
// Even though we've already checked for response.filterInfo, since
|
|
222
|
+
// we're in a callback TypeScript still considers accessing it
|
|
223
|
+
// unsafe, so do another check.
|
|
224
|
+
invariant(!!response.filterInfo, '')
|
|
225
|
+
|
|
226
|
+
invariant(
|
|
227
|
+
previous != null,
|
|
228
|
+
'handleResponse called without filterInfo',
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const extra: Partial<FilterInfo<TFilter>> = {}
|
|
232
|
+
const {
|
|
233
|
+
page,
|
|
234
|
+
offset,
|
|
235
|
+
totalResults,
|
|
236
|
+
pageSize,
|
|
237
|
+
nextCursor,
|
|
238
|
+
...filterInfoFromResponse
|
|
239
|
+
} = response.filterInfo
|
|
240
|
+
|
|
241
|
+
const pageSizeNumber = Number(pageSize || previous.pageSize)
|
|
242
|
+
|
|
243
|
+
if (page != null && offset == null) {
|
|
244
|
+
extra.offset = getOffsetFromPage(page, pageSizeNumber)
|
|
245
|
+
extra.page = page
|
|
246
|
+
// If there's an offset but no page, set the page.
|
|
247
|
+
} else if (page == null && offset != null) {
|
|
248
|
+
const offsetNumber = Number(offset)
|
|
249
|
+
extra.offset = offsetNumber
|
|
250
|
+
extra.page = getPageFromOffset(offsetNumber, pageSizeNumber)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// `null` / `undefined` are valid here, it means there is zero
|
|
254
|
+
// results, so we can't just check if it's truthy, we need to check
|
|
255
|
+
// for presence.
|
|
256
|
+
if ('totalResults' in response.filterInfo) {
|
|
257
|
+
extra.totalResults = Number(totalResults || 0)
|
|
258
|
+
extra.totalPages = Math.ceil(extra.totalResults / pageSizeNumber)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// `null` / `undefined` are valid here, it means there is no next
|
|
262
|
+
// cursor, so we can't just check if it's truthy, we need to check
|
|
263
|
+
// for presence.
|
|
264
|
+
if ('nextCursor' in response.filterInfo) {
|
|
265
|
+
extra.nextCursor = nextCursor
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
...previous,
|
|
270
|
+
...filterInfoFromResponse,
|
|
271
|
+
...extra,
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (response.data) {
|
|
277
|
+
setData(response.data)
|
|
278
|
+
getFilterStore(ctxRef.current.store).saveData(
|
|
279
|
+
namespace,
|
|
280
|
+
response.data,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (isPromise(response)) {
|
|
286
|
+
response.then(handleResponse).finally(() => setIsLoading(false))
|
|
287
|
+
} else {
|
|
288
|
+
handleResponse(response)
|
|
289
|
+
setIsLoading(false)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let timeout: ReturnType<typeof setTimeout> | undefined = undefined
|
|
294
|
+
|
|
295
|
+
if (filterInfo.shouldRunImmediately) {
|
|
296
|
+
makeRequest()
|
|
297
|
+
} else {
|
|
298
|
+
timeout = setTimeout(
|
|
299
|
+
makeRequest,
|
|
300
|
+
ctxRef.current.debounceDuration != null
|
|
301
|
+
? ctxRef.current.debounceDuration
|
|
302
|
+
: 500,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return () => {
|
|
307
|
+
if (timeout) {
|
|
308
|
+
clearTimeout(timeout)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}, [filterInfo, namespace])
|
|
312
|
+
|
|
313
|
+
const api: FilterApi<TFilter, TResult> = {
|
|
314
|
+
isLoading,
|
|
315
|
+
data,
|
|
316
|
+
doesFilterExist: doesFilterExist.current,
|
|
317
|
+
filterInfo: filterInfo || buildDefaultFilterInfo(options.defaultFilterInfo),
|
|
318
|
+
updateReason: updateReasonRef.current,
|
|
319
|
+
|
|
320
|
+
updateFilter: useCallback(
|
|
321
|
+
(filter: Partial<TFilter>, shouldRunImmediately: boolean = false) => {
|
|
322
|
+
updateReasonRef.current = 'filter'
|
|
323
|
+
|
|
324
|
+
setFilterInfoState((previous) => {
|
|
325
|
+
invariant(previous != null, 'updateFilter called without filterInfo')
|
|
326
|
+
|
|
327
|
+
const nextFilter = {
|
|
328
|
+
...previous.filter,
|
|
329
|
+
...filter,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (shallowEqual(previous.filter, nextFilter)) {
|
|
333
|
+
return previous
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
...previous,
|
|
338
|
+
offset: 0,
|
|
339
|
+
page: 1,
|
|
340
|
+
totalResults: 1,
|
|
341
|
+
totalPages: 1,
|
|
342
|
+
filter: nextFilter,
|
|
343
|
+
lastRefreshAt: new Date().getTime(),
|
|
344
|
+
cursor: undefined,
|
|
345
|
+
nextCursor: undefined,
|
|
346
|
+
shouldRunImmediately,
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
},
|
|
350
|
+
[],
|
|
351
|
+
),
|
|
352
|
+
|
|
353
|
+
resetFilter: useCallback((shouldRunImmediately: boolean = false) => {
|
|
354
|
+
updateReasonRef.current = 'filter'
|
|
355
|
+
|
|
356
|
+
setFilterInfoState({
|
|
357
|
+
...buildDefaultFilterInfo(ctxRef.current.defaultFilterInfo),
|
|
358
|
+
lastRefreshAt: new Date().getTime(),
|
|
359
|
+
shouldRunImmediately,
|
|
360
|
+
})
|
|
361
|
+
}, []),
|
|
362
|
+
|
|
363
|
+
setOffset: useCallback(
|
|
364
|
+
(offset: number | string, shouldRunImmediately: boolean = false) => {
|
|
365
|
+
updateReasonRef.current = 'pagination'
|
|
366
|
+
|
|
367
|
+
setFilterInfoState((previous) => {
|
|
368
|
+
invariant(previous != null, 'setOffset called without filterInfo')
|
|
369
|
+
|
|
370
|
+
const offsetNumber = Number(offset)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
...previous,
|
|
374
|
+
offset: offsetNumber,
|
|
375
|
+
page: getPageFromOffset(offsetNumber, previous.pageSize),
|
|
376
|
+
lastRefreshAt: new Date().getTime(),
|
|
377
|
+
shouldRunImmediately,
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
},
|
|
381
|
+
[],
|
|
382
|
+
),
|
|
383
|
+
|
|
384
|
+
setPage: useCallback(
|
|
385
|
+
(page: number | string, shouldRunImmediately: boolean = false) => {
|
|
386
|
+
updateReasonRef.current = 'pagination'
|
|
387
|
+
|
|
388
|
+
setFilterInfoState((previous) => {
|
|
389
|
+
invariant(previous != null, 'setPage called without filterInfo')
|
|
390
|
+
|
|
391
|
+
const pageNumber = Number(page)
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
...previous,
|
|
395
|
+
offset: getOffsetFromPage(pageNumber, previous.pageSize),
|
|
396
|
+
page: pageNumber,
|
|
397
|
+
lastRefreshAt: new Date().getTime(),
|
|
398
|
+
shouldRunImmediately,
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
},
|
|
402
|
+
[],
|
|
403
|
+
),
|
|
404
|
+
|
|
405
|
+
setPageSize: useCallback(
|
|
406
|
+
(pageSize: number | string, shouldRunImmediately: boolean = false) => {
|
|
407
|
+
updateReasonRef.current = 'pagination'
|
|
408
|
+
|
|
409
|
+
setFilterInfoState((previous) => {
|
|
410
|
+
invariant(previous != null, 'setPageSize called without filterInfo')
|
|
411
|
+
|
|
412
|
+
const pageSizeNumber = Number(pageSize)
|
|
413
|
+
const page = getPageFromOffset(previous.offset, pageSizeNumber)
|
|
414
|
+
const offset = getOffsetFromPage(page, pageSizeNumber)
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
...previous,
|
|
418
|
+
pageSize: pageSizeNumber,
|
|
419
|
+
offset,
|
|
420
|
+
page,
|
|
421
|
+
lastRefreshAt: new Date().getTime(),
|
|
422
|
+
shouldRunImmediately,
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
},
|
|
426
|
+
[],
|
|
427
|
+
),
|
|
428
|
+
|
|
429
|
+
setSort: useCallback(
|
|
430
|
+
(sort: string | undefined, shouldRunImmediately: boolean = false) => {
|
|
431
|
+
updateReasonRef.current = 'filter'
|
|
432
|
+
|
|
433
|
+
setFilterInfoState((previous) => {
|
|
434
|
+
invariant(previous != null, 'setSort called without filterInfo')
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
...previous,
|
|
438
|
+
sort,
|
|
439
|
+
lastRefreshAt: new Date().getTime(),
|
|
440
|
+
shouldRunImmediately,
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
},
|
|
444
|
+
[],
|
|
445
|
+
),
|
|
446
|
+
|
|
447
|
+
setCursor: useCallback(
|
|
448
|
+
(
|
|
449
|
+
cursor: string | null | undefined,
|
|
450
|
+
shouldRunImmediately: boolean = false,
|
|
451
|
+
) => {
|
|
452
|
+
setFilterInfoState((previous) => {
|
|
453
|
+
updateReasonRef.current = 'pagination'
|
|
454
|
+
|
|
455
|
+
invariant(previous != null, 'setCursor called without filterInfo')
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
...previous,
|
|
459
|
+
cursor,
|
|
460
|
+
lastRefreshAt: new Date().getTime(),
|
|
461
|
+
shouldRunImmediately,
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
},
|
|
465
|
+
[],
|
|
466
|
+
),
|
|
467
|
+
|
|
468
|
+
forceRefresh: useCallback((shouldRunImmediately: boolean = false) => {
|
|
469
|
+
updateReasonRef.current = 'filter'
|
|
470
|
+
|
|
471
|
+
setFilterInfoState((previous) => {
|
|
472
|
+
invariant(previous != null, 'forceRefresh called without filterInfo')
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
...previous,
|
|
476
|
+
lastRefreshAt: new Date().getTime(),
|
|
477
|
+
shouldRunImmediately,
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
}, []),
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return api
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export default useFilter
|
|
487
|
+
|
|
488
|
+
export interface FilterInfo<TFilter extends Record<string, unknown>> {
|
|
489
|
+
filter: TFilter
|
|
490
|
+
offset: number
|
|
491
|
+
page: number
|
|
492
|
+
sort?: string
|
|
493
|
+
pageSize: number
|
|
494
|
+
lastRefreshAt: number
|
|
495
|
+
totalResults: number
|
|
496
|
+
totalPages: number
|
|
497
|
+
shouldRunImmediately: boolean
|
|
498
|
+
cursor?: string | null
|
|
499
|
+
nextCursor?: string | null
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
type UseFilterOnChangeResult<
|
|
503
|
+
TFilter extends Record<string, unknown>,
|
|
504
|
+
TResult,
|
|
505
|
+
> = void | {
|
|
506
|
+
filterInfo?: Partial<
|
|
507
|
+
Omit<FilterInfo<TFilter>, 'totalResults' | 'offset' | 'pageSize'> & {
|
|
508
|
+
totalResults: string | number
|
|
509
|
+
offset: string | number
|
|
510
|
+
pageSize: string | number
|
|
511
|
+
}
|
|
512
|
+
>
|
|
513
|
+
data?: TResult
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function invariant(input: boolean, message: string): asserts input {
|
|
517
|
+
if (!input) {
|
|
518
|
+
throw new Error(`Invariant error: ${message}`)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function isPromise(t?: any): t is Promise<any> {
|
|
523
|
+
return !!t?.then
|
|
524
|
+
}
|