@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.1
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/__tests__/_helpers/first-page-runtime.ts +184 -0
- package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +210 -4
- package/__tests__/data-source-request-locale.test.ts +4 -3
- package/__tests__/fetchers.test.ts +7 -3
- package/__tests__/fixed-list-sort.test.ts +1 -1
- package/__tests__/infinite-scroll.test.ts +363 -9
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/page-data-visitor-read.test.ts +196 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +2 -2
- package/__tests__/plain-list-starting-sort.test.ts +323 -0
- package/__tests__/product-price-sort.ts +11 -6
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/refetch-loading-state.test.ts +158 -2
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +34 -8
- package/__tests__/starting-sort-first-page.test.ts +285 -0
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- package/dist/cjs/cache/ast.d.ts +2 -0
- package/dist/cjs/cache/ast.d.ts.map +1 -1
- package/dist/cjs/cache/ast.js +5 -2
- package/dist/cjs/cache/ast.js.map +1 -1
- package/dist/cjs/count-effect.d.ts +7 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +15 -5
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +10 -2
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
- package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/cjs/fetchers/pg-client-import.js +15 -0
- package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +2 -1
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +89 -4
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +243 -33
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/list-sort-order.d.ts +14 -0
- package/dist/cjs/list-sort-order.d.ts.map +1 -0
- package/dist/cjs/list-sort-order.js +36 -0
- package/dist/cjs/list-sort-order.js.map +1 -0
- package/dist/cjs/loading-state.d.ts +23 -2
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +50 -15
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/pagination-controls.d.ts +22 -3
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +29 -3
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +336 -164
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/range-control.d.ts +45 -0
- package/dist/cjs/range-control.d.ts.map +1 -0
- package/dist/cjs/range-control.js +165 -0
- package/dist/cjs/range-control.js.map +1 -0
- package/dist/cjs/row-order.d.ts +3 -2
- package/dist/cjs/row-order.d.ts.map +1 -1
- package/dist/cjs/row-order.js +5 -4
- package/dist/cjs/row-order.js.map +1 -1
- package/dist/cjs/search-input.d.ts +42 -0
- package/dist/cjs/search-input.d.ts.map +1 -0
- package/dist/cjs/search-input.js +158 -0
- package/dist/cjs/search-input.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +53 -1
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +105 -13
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/booking-item.d.ts +24 -0
- package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
- package/dist/cjs/transformations/booking-item.js +27 -0
- package/dist/cjs/transformations/booking-item.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +4 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -5
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +3 -2
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +48 -18
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/cache/ast.d.ts +2 -0
- package/dist/esm/cache/ast.d.ts.map +1 -1
- package/dist/esm/cache/ast.js +3 -1
- package/dist/esm/cache/ast.js.map +1 -1
- package/dist/esm/count-effect.d.ts +7 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +15 -5
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +10 -2
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
- package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/esm/fetchers/pg-client-import.js +12 -0
- package/dist/esm/fetchers/pg-client-import.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +2 -1
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +89 -4
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +237 -32
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/list-sort-order.d.ts +14 -0
- package/dist/esm/list-sort-order.d.ts.map +1 -0
- package/dist/esm/list-sort-order.js +32 -0
- package/dist/esm/list-sort-order.js.map +1 -0
- package/dist/esm/loading-state.d.ts +23 -2
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +50 -15
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/pagination-controls.d.ts +22 -3
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +28 -2
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +340 -168
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/range-control.d.ts +45 -0
- package/dist/esm/range-control.d.ts.map +1 -0
- package/dist/esm/range-control.js +137 -0
- package/dist/esm/range-control.js.map +1 -0
- package/dist/esm/row-order.d.ts +3 -2
- package/dist/esm/row-order.d.ts.map +1 -1
- package/dist/esm/row-order.js +5 -4
- package/dist/esm/row-order.js.map +1 -1
- package/dist/esm/search-input.d.ts +42 -0
- package/dist/esm/search-input.d.ts.map +1 -0
- package/dist/esm/search-input.js +129 -0
- package/dist/esm/search-input.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +53 -1
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +100 -12
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/booking-item.d.ts +24 -0
- package/dist/esm/transformations/booking-item.d.ts.map +1 -0
- package/dist/esm/transformations/booking-item.js +23 -0
- package/dist/esm/transformations/booking-item.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +4 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -5
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +3 -2
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +49 -19
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/cache/ast.ts +4 -1
- package/src/count-effect.ts +31 -8
- package/src/data-source-fetchers.ts +33 -14
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +15 -9
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/teleport.ts +19 -10
- package/src/fetchers/utils/browser-row-policy.ts +79 -5
- package/src/index.ts +8 -2
- package/src/infinite-scroll.ts +420 -81
- package/src/list-sort-order.ts +34 -0
- package/src/loading-state.ts +85 -13
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +532 -218
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +5 -4
- package/src/search-input.ts +190 -0
- package/src/sort-utils.ts +164 -21
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/index.ts +25 -2
- package/src/utils.ts +55 -18
package/src/infinite-scroll.ts
CHANGED
|
@@ -6,15 +6,30 @@ import * as types from '@babel/types'
|
|
|
6
6
|
* The accumulation lives inside the generated `fetchData` closure rather than
|
|
7
7
|
* around the provider. `DataProvider` renders whatever `fetchData` resolves
|
|
8
8
|
* with, so returning "every page fetched so far, in order" is enough to make it
|
|
9
|
-
* append — no change to the runtime component library, and
|
|
9
|
+
* append — no change to the runtime component library, and
|
|
10
10
|
* `persistDataDuringLoading` keeps the rows already on screen while the next
|
|
11
|
-
* page is in flight
|
|
11
|
+
* page is in flight (`buildAppendingRequestCheck`: the loading slot replaces
|
|
12
|
+
* them only for a list loading afresh).
|
|
12
13
|
*
|
|
13
14
|
* A `useRef` holds the pages rather than a state, because writing accumulated
|
|
14
15
|
* rows into state from inside `fetchData` would re-render the provider mid-fetch
|
|
15
16
|
* and re-enter it. The ref is keyed by a SIGNATURE of every fetch param except
|
|
16
17
|
* the page, so a filter, sort or search change starts a fresh list instead of
|
|
17
18
|
* interleaving rows from two different queries.
|
|
19
|
+
*
|
|
20
|
+
* `fetchData` records the query and the page the list stands on BEFORE it
|
|
21
|
+
* requests them (`latest`), and only the answer to that request changes the
|
|
22
|
+
* list. A filter, sort or search change made past page 1 fires the old page
|
|
23
|
+
* under the new query first, then page 1 once the page is reset: kept, the
|
|
24
|
+
* stray page sat in the list, the page counter said 1, and the next "Load
|
|
25
|
+
* more" fetched a page already on show. `DataProvider` also renders whichever
|
|
26
|
+
* answer arrives LAST, so a late answer to an earlier query must not replace
|
|
27
|
+
* the list either.
|
|
28
|
+
*
|
|
29
|
+
* On a page, the list starts from the rows `getStaticProps` already fetched
|
|
30
|
+
* (`buildFirstPageDeclaration`, `buildFirstPageSeedEffect`): the server HTML
|
|
31
|
+
* carries them instead of the loading state, the accumulator holds them as
|
|
32
|
+
* page 1, and the browser's first request is page 2.
|
|
18
33
|
*/
|
|
19
34
|
|
|
20
35
|
// tslint:disable:no-any
|
|
@@ -25,6 +40,7 @@ export interface InfiniteScrollVars {
|
|
|
25
40
|
setHasMoreFlagVar: string
|
|
26
41
|
hasMoreVar: string
|
|
27
42
|
sentinelRefVar: string
|
|
43
|
+
firstPageVar: string
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export const getInfiniteScrollVars = (index: number): InfiniteScrollVars => ({
|
|
@@ -33,6 +49,7 @@ export const getInfiniteScrollVars = (index: number): InfiniteScrollVars => ({
|
|
|
33
49
|
setHasMoreFlagVar: `setDs_${index}_hasMoreFlag`,
|
|
34
50
|
hasMoreVar: `ds_${index}_hasMore`,
|
|
35
51
|
sentinelRefVar: `ds_${index}_sentinelRef`,
|
|
52
|
+
firstPageVar: `ds_${index}_firstPage`,
|
|
36
53
|
})
|
|
37
54
|
|
|
38
55
|
/**
|
|
@@ -62,6 +79,7 @@ export const buildInfiniteScrollDeclarations = (
|
|
|
62
79
|
types.objectExpression([
|
|
63
80
|
types.objectProperty(types.identifier('sig'), types.stringLiteral('')),
|
|
64
81
|
types.objectProperty(types.identifier('pages'), types.objectExpression([])),
|
|
82
|
+
types.objectProperty(types.identifier('latest'), types.nullLiteral()),
|
|
65
83
|
]),
|
|
66
84
|
])
|
|
67
85
|
),
|
|
@@ -161,11 +179,226 @@ const writeAtPath = (
|
|
|
161
179
|
])
|
|
162
180
|
}
|
|
163
181
|
|
|
182
|
+
/** `JSON.stringify({ ...params, page: 0 })`: everything that identifies the query but the page. */
|
|
183
|
+
const buildQuerySignature = (paramsVar: string = 'params'): types.CallExpression =>
|
|
184
|
+
types.callExpression(
|
|
185
|
+
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
186
|
+
[
|
|
187
|
+
types.objectExpression([
|
|
188
|
+
types.spreadElement(types.identifier(paramsVar)),
|
|
189
|
+
types.objectProperty(types.identifier('page'), types.numericLiteral(0)),
|
|
190
|
+
]),
|
|
191
|
+
]
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
/** `Number(params.page) || 1` */
|
|
195
|
+
const buildRequestedPage = (): types.LogicalExpression =>
|
|
196
|
+
types.logicalExpression(
|
|
197
|
+
'||',
|
|
198
|
+
types.callExpression(types.identifier('Number'), [
|
|
199
|
+
types.memberExpression(types.identifier('params'), types.identifier('page')),
|
|
200
|
+
]),
|
|
201
|
+
types.numericLiteral(1)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const accumCurrentOf = (vars: InfiniteScrollVars): types.MemberExpression =>
|
|
205
|
+
types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* What `fetchData` runs before its request: the query and the page the list
|
|
209
|
+
* now stands on.
|
|
210
|
+
*
|
|
211
|
+
* ds_0_accumRef.current.latest = { sig: JSON.stringify({ ...params, page: 0 }), page: Number(params.page) || 1 }
|
|
212
|
+
*/
|
|
213
|
+
export const buildAccumulatingRequestMark = (vars: InfiniteScrollVars): types.Statement =>
|
|
214
|
+
types.expressionStatement(
|
|
215
|
+
types.assignmentExpression(
|
|
216
|
+
'=',
|
|
217
|
+
types.memberExpression(accumCurrentOf(vars), types.identifier('latest')),
|
|
218
|
+
types.objectExpression([
|
|
219
|
+
types.objectProperty(types.identifier('sig'), buildQuerySignature()),
|
|
220
|
+
types.objectProperty(types.identifier('page'), buildRequestedPage()),
|
|
221
|
+
])
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Whether the request `fetchData` is about to make only adds a page to the rows
|
|
227
|
+
* on show: a page past the first, of the query the accumulator holds. Read
|
|
228
|
+
* after `buildAccumulatingRequestMark` has recorded that request.
|
|
229
|
+
*
|
|
230
|
+
* ds_0_accumRef.current.latest.page > 1 && ds_0_accumRef.current.latest.sig === ds_0_accumRef.current.sig
|
|
231
|
+
*
|
|
232
|
+
* A filter, sort or search change is no append even past page 1 — the page it
|
|
233
|
+
* fires before the reset belongs to another query — and neither is a list's
|
|
234
|
+
* first page: both load the list afresh, behind its loading slot.
|
|
235
|
+
*/
|
|
236
|
+
export const buildAppendingRequestCheck = (vars: InfiniteScrollVars): types.Expression => {
|
|
237
|
+
const latestOf = (key: string): types.MemberExpression =>
|
|
238
|
+
types.memberExpression(
|
|
239
|
+
types.memberExpression(accumCurrentOf(vars), types.identifier('latest')),
|
|
240
|
+
types.identifier(key)
|
|
241
|
+
)
|
|
242
|
+
return types.logicalExpression(
|
|
243
|
+
'&&',
|
|
244
|
+
types.binaryExpression('>', latestOf('page'), types.numericLiteral(1)),
|
|
245
|
+
types.binaryExpression(
|
|
246
|
+
'===',
|
|
247
|
+
latestOf('sig'),
|
|
248
|
+
types.memberExpression(accumCurrentOf(vars), types.identifier('sig'))
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The rows inside a server payload: the payload itself, or what sits at
|
|
255
|
+
* `itemsPath` in one that wraps them. `<rows>?.length > 0` is how the caller
|
|
256
|
+
* asks whether the server found any.
|
|
257
|
+
*/
|
|
258
|
+
export const buildServerRowsExpression = (
|
|
259
|
+
payload: types.Expression,
|
|
260
|
+
itemsPath?: string[]
|
|
261
|
+
): types.Expression => (itemsPath?.length ? readAtPath(payload, itemsPath) : payload)
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The first page the list starts from, when its page already fetched it:
|
|
265
|
+
*
|
|
266
|
+
* const ds_0_firstPage = useMemo(() => <starts> ? props?.items_ds_0 : undefined, [<instance deps>])
|
|
267
|
+
*
|
|
268
|
+
* Handed to `DataProvider` as `initialData`, so the server HTML holds the rows
|
|
269
|
+
* rather than the loading state, and the provider skips its own fetch of page 1.
|
|
270
|
+
*
|
|
271
|
+
* ⛔ Frozen for the provider's lifetime. `DataProvider` copies `initialData`
|
|
272
|
+
* into what it shows every time the value changes, so a value that moved with
|
|
273
|
+
* the page — the paged lists' `page === 1 ? … : undefined`, fine for them since
|
|
274
|
+
* they remount per page — would wipe the appended rows on the first scroll.
|
|
275
|
+
* `instanceDeps` are what remounts the provider (its `key`): nothing for a list
|
|
276
|
+
* without search, the applied query for one with search.
|
|
277
|
+
*/
|
|
278
|
+
export const buildFirstPageDeclaration = (
|
|
279
|
+
vars: InfiniteScrollVars,
|
|
280
|
+
startsFromServerRows: types.Expression,
|
|
281
|
+
serverPayload: types.Expression,
|
|
282
|
+
instanceDeps: types.Expression[]
|
|
283
|
+
): types.Statement =>
|
|
284
|
+
types.variableDeclaration('const', [
|
|
285
|
+
types.variableDeclarator(
|
|
286
|
+
types.identifier(vars.firstPageVar),
|
|
287
|
+
types.callExpression(types.identifier('useMemo'), [
|
|
288
|
+
types.arrowFunctionExpression(
|
|
289
|
+
[],
|
|
290
|
+
types.conditionalExpression(
|
|
291
|
+
startsFromServerRows,
|
|
292
|
+
serverPayload,
|
|
293
|
+
types.identifier('undefined')
|
|
294
|
+
)
|
|
295
|
+
),
|
|
296
|
+
types.arrayExpression(instanceDeps),
|
|
297
|
+
])
|
|
298
|
+
),
|
|
299
|
+
])
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Makes the server rows page 1 of the accumulator whenever the provider is
|
|
303
|
+
* handed them instead of fetching:
|
|
304
|
+
*
|
|
305
|
+
* useEffect(() => {
|
|
306
|
+
* if (ds_0_firstPage === undefined) return
|
|
307
|
+
* const rows = ds_0_firstPage || []
|
|
308
|
+
* const sig = JSON.stringify({ ...ds_0_params, page: 0 })
|
|
309
|
+
* ds_0_accumRef.current = { sig, pages: { 1: rows }, latest: { sig, page: 1 } }
|
|
310
|
+
* setDs_0_hasMoreFlag(rows.length >= 20)
|
|
311
|
+
* }, [ds_0_firstPage])
|
|
312
|
+
*
|
|
313
|
+
* Without it the accumulator never sees page 1 (the provider skipped that
|
|
314
|
+
* fetch), and the answer to page 2 would REPLACE the rows on screen with page 2
|
|
315
|
+
* alone. The signature is the one `fetchData` computes for the same params, so
|
|
316
|
+
* the next page appends to these rows while a filter, sort or search change
|
|
317
|
+
* still starts a fresh list; `latest` keeps an answer still in flight from an
|
|
318
|
+
* earlier query from changing anything. A short first page is the last one, as
|
|
319
|
+
* it would be had it been fetched.
|
|
320
|
+
*/
|
|
321
|
+
export const buildFirstPageSeedEffect = (
|
|
322
|
+
vars: InfiniteScrollVars,
|
|
323
|
+
paramsVar: string,
|
|
324
|
+
perPage: number,
|
|
325
|
+
itemsPath?: string[]
|
|
326
|
+
): types.Statement =>
|
|
327
|
+
types.expressionStatement(
|
|
328
|
+
types.callExpression(types.identifier('useEffect'), [
|
|
329
|
+
types.arrowFunctionExpression(
|
|
330
|
+
[],
|
|
331
|
+
types.blockStatement([
|
|
332
|
+
types.ifStatement(
|
|
333
|
+
types.binaryExpression(
|
|
334
|
+
'===',
|
|
335
|
+
types.identifier(vars.firstPageVar),
|
|
336
|
+
types.identifier('undefined')
|
|
337
|
+
),
|
|
338
|
+
types.returnStatement()
|
|
339
|
+
),
|
|
340
|
+
types.variableDeclaration('const', [
|
|
341
|
+
types.variableDeclarator(
|
|
342
|
+
types.identifier('rows'),
|
|
343
|
+
types.logicalExpression(
|
|
344
|
+
'||',
|
|
345
|
+
buildServerRowsExpression(types.identifier(vars.firstPageVar), itemsPath),
|
|
346
|
+
types.arrayExpression([])
|
|
347
|
+
)
|
|
348
|
+
),
|
|
349
|
+
]),
|
|
350
|
+
types.variableDeclaration('const', [
|
|
351
|
+
types.variableDeclarator(types.identifier('sig'), buildQuerySignature(paramsVar)),
|
|
352
|
+
]),
|
|
353
|
+
types.expressionStatement(
|
|
354
|
+
types.assignmentExpression(
|
|
355
|
+
'=',
|
|
356
|
+
accumCurrentOf(vars),
|
|
357
|
+
types.objectExpression([
|
|
358
|
+
types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
|
|
359
|
+
types.objectProperty(
|
|
360
|
+
types.identifier('pages'),
|
|
361
|
+
types.objectExpression([
|
|
362
|
+
types.objectProperty(types.numericLiteral(1), types.identifier('rows')),
|
|
363
|
+
])
|
|
364
|
+
),
|
|
365
|
+
types.objectProperty(
|
|
366
|
+
types.identifier('latest'),
|
|
367
|
+
types.objectExpression([
|
|
368
|
+
types.objectProperty(
|
|
369
|
+
types.identifier('sig'),
|
|
370
|
+
types.identifier('sig'),
|
|
371
|
+
false,
|
|
372
|
+
true
|
|
373
|
+
),
|
|
374
|
+
types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
|
|
375
|
+
])
|
|
376
|
+
),
|
|
377
|
+
])
|
|
378
|
+
)
|
|
379
|
+
),
|
|
380
|
+
types.expressionStatement(
|
|
381
|
+
types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
|
|
382
|
+
types.binaryExpression(
|
|
383
|
+
'>=',
|
|
384
|
+
types.memberExpression(types.identifier('rows'), types.identifier('length')),
|
|
385
|
+
types.numericLiteral(perPage)
|
|
386
|
+
),
|
|
387
|
+
])
|
|
388
|
+
),
|
|
389
|
+
])
|
|
390
|
+
),
|
|
391
|
+
types.arrayExpression([types.identifier(vars.firstPageVar)]),
|
|
392
|
+
])
|
|
393
|
+
)
|
|
394
|
+
|
|
164
395
|
/**
|
|
165
396
|
* The `.then(...)` tail that accumulates a page and reports whether more exist.
|
|
166
397
|
*
|
|
167
398
|
* Returned as the resolved value of `fetchData`, so `DataProvider` stores the
|
|
168
|
-
* whole accumulated list as its data
|
|
399
|
+
* whole accumulated list as its data: the pages of the query on show, first
|
|
400
|
+
* to the page it stands on. An answer to anything but the latest request
|
|
401
|
+
* (`buildAccumulatingRequestMark`) is not kept, and gets the list as it is.
|
|
169
402
|
*
|
|
170
403
|
* `itemsPath` is where the rows sit inside a payload that wraps them
|
|
171
404
|
* (`{ items: [...], total }`): the rows are read there and the accumulated list
|
|
@@ -178,10 +411,11 @@ export const buildAccumulatingResponseHandler = (
|
|
|
178
411
|
perPage: number,
|
|
179
412
|
itemsPath?: string[]
|
|
180
413
|
): types.ArrowFunctionExpression => {
|
|
181
|
-
const accumCurrent = (): types.MemberExpression =>
|
|
182
|
-
types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
|
|
414
|
+
const accumCurrent = (): types.MemberExpression => accumCurrentOf(vars)
|
|
183
415
|
const accumPages = (): types.MemberExpression =>
|
|
184
416
|
types.memberExpression(accumCurrent(), types.identifier('pages'))
|
|
417
|
+
const latestOf = (key: string): types.MemberExpression =>
|
|
418
|
+
types.memberExpression(types.identifier('latest'), types.identifier(key))
|
|
185
419
|
const payload = (): types.OptionalMemberExpression =>
|
|
186
420
|
types.optionalMemberExpression(
|
|
187
421
|
types.identifier('response'),
|
|
@@ -191,6 +425,8 @@ export const buildAccumulatingResponseHandler = (
|
|
|
191
425
|
)
|
|
192
426
|
const rowsPath = itemsPath?.length ? itemsPath : undefined
|
|
193
427
|
|
|
428
|
+
// Every page held up to `last`, flattened in page order — a numeric sort, so
|
|
429
|
+
// page 10 does not land between 1 and 2.
|
|
194
430
|
const accumulatedRows = types.callExpression(
|
|
195
431
|
types.memberExpression(
|
|
196
432
|
types.callExpression(
|
|
@@ -198,12 +434,23 @@ export const buildAccumulatingResponseHandler = (
|
|
|
198
434
|
types.callExpression(
|
|
199
435
|
types.memberExpression(
|
|
200
436
|
types.callExpression(
|
|
201
|
-
types.memberExpression(
|
|
202
|
-
|
|
437
|
+
types.memberExpression(
|
|
438
|
+
types.callExpression(
|
|
439
|
+
types.memberExpression(types.identifier('Object'), types.identifier('keys')),
|
|
440
|
+
[accumPages()]
|
|
441
|
+
),
|
|
442
|
+
types.identifier('map')
|
|
443
|
+
),
|
|
444
|
+
[types.identifier('Number')]
|
|
203
445
|
),
|
|
204
|
-
types.identifier('
|
|
446
|
+
types.identifier('filter')
|
|
205
447
|
),
|
|
206
|
-
[
|
|
448
|
+
[
|
|
449
|
+
types.arrowFunctionExpression(
|
|
450
|
+
[types.identifier('p')],
|
|
451
|
+
types.binaryExpression('<=', types.identifier('p'), types.identifier('last'))
|
|
452
|
+
),
|
|
453
|
+
]
|
|
207
454
|
),
|
|
208
455
|
types.identifier('sort')
|
|
209
456
|
),
|
|
@@ -242,73 +489,97 @@ export const buildAccumulatingResponseHandler = (
|
|
|
242
489
|
),
|
|
243
490
|
]),
|
|
244
491
|
types.variableDeclaration('const', [
|
|
245
|
-
types.variableDeclarator(
|
|
246
|
-
types.identifier('page'),
|
|
247
|
-
types.logicalExpression(
|
|
248
|
-
'||',
|
|
249
|
-
types.callExpression(types.identifier('Number'), [
|
|
250
|
-
types.memberExpression(types.identifier('params'), types.identifier('page')),
|
|
251
|
-
]),
|
|
252
|
-
types.numericLiteral(1)
|
|
253
|
-
)
|
|
254
|
-
),
|
|
492
|
+
types.variableDeclarator(types.identifier('page'), buildRequestedPage()),
|
|
255
493
|
]),
|
|
256
494
|
// Everything that identifies the QUERY, with the page zeroed out: two
|
|
257
495
|
// fetches that differ only by page belong to the same accumulated list.
|
|
496
|
+
types.variableDeclaration('const', [
|
|
497
|
+
types.variableDeclarator(types.identifier('sig'), buildQuerySignature()),
|
|
498
|
+
]),
|
|
499
|
+
// The request the list stands on now, recorded when it was made.
|
|
258
500
|
types.variableDeclaration('const', [
|
|
259
501
|
types.variableDeclarator(
|
|
260
|
-
types.identifier('
|
|
261
|
-
types.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
]
|
|
502
|
+
types.identifier('latest'),
|
|
503
|
+
types.logicalExpression(
|
|
504
|
+
'||',
|
|
505
|
+
types.memberExpression(accumCurrent(), types.identifier('latest')),
|
|
506
|
+
types.objectExpression([
|
|
507
|
+
types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
|
|
508
|
+
types.objectProperty(types.identifier('page'), types.identifier('page'), false, true),
|
|
509
|
+
])
|
|
269
510
|
)
|
|
270
511
|
),
|
|
271
512
|
]),
|
|
513
|
+
// Only its answer is kept: an earlier query's, or a page past the one a
|
|
514
|
+
// filter / sort / search change restarted the list at, changes nothing.
|
|
272
515
|
types.ifStatement(
|
|
273
|
-
types.
|
|
274
|
-
'
|
|
275
|
-
types.
|
|
276
|
-
types.identifier('
|
|
516
|
+
types.logicalExpression(
|
|
517
|
+
'&&',
|
|
518
|
+
types.binaryExpression('===', types.identifier('sig'), latestOf('sig')),
|
|
519
|
+
types.binaryExpression('<=', types.identifier('page'), latestOf('page'))
|
|
277
520
|
),
|
|
278
521
|
types.blockStatement([
|
|
522
|
+
types.ifStatement(
|
|
523
|
+
types.binaryExpression(
|
|
524
|
+
'!==',
|
|
525
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
526
|
+
types.identifier('sig')
|
|
527
|
+
),
|
|
528
|
+
types.blockStatement([
|
|
529
|
+
types.expressionStatement(
|
|
530
|
+
types.assignmentExpression(
|
|
531
|
+
'=',
|
|
532
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
533
|
+
types.identifier('sig')
|
|
534
|
+
)
|
|
535
|
+
),
|
|
536
|
+
types.expressionStatement(
|
|
537
|
+
types.assignmentExpression('=', accumPages(), types.objectExpression([]))
|
|
538
|
+
),
|
|
539
|
+
])
|
|
540
|
+
),
|
|
279
541
|
types.expressionStatement(
|
|
280
542
|
types.assignmentExpression(
|
|
281
543
|
'=',
|
|
282
|
-
|
|
283
|
-
types.
|
|
284
|
-
types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
|
|
285
|
-
types.objectProperty(types.identifier('pages'), types.objectExpression([])),
|
|
286
|
-
])
|
|
544
|
+
types.memberExpression(accumPages(), types.identifier('page'), true),
|
|
545
|
+
types.identifier('rows')
|
|
287
546
|
)
|
|
288
547
|
),
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
types.memberExpression(types.identifier('rows'), types.identifier('length')),
|
|
305
|
-
types.numericLiteral(perPage)
|
|
548
|
+
// A page that came back short is the last one. This is the only
|
|
549
|
+
// end-of-data signal a data source that cannot count ever gives.
|
|
550
|
+
types.ifStatement(
|
|
551
|
+
types.binaryExpression('===', types.identifier('page'), latestOf('page')),
|
|
552
|
+
types.blockStatement([
|
|
553
|
+
types.expressionStatement(
|
|
554
|
+
types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
|
|
555
|
+
types.binaryExpression(
|
|
556
|
+
'>=',
|
|
557
|
+
types.memberExpression(types.identifier('rows'), types.identifier('length')),
|
|
558
|
+
types.numericLiteral(perPage)
|
|
559
|
+
),
|
|
560
|
+
])
|
|
561
|
+
),
|
|
562
|
+
])
|
|
306
563
|
),
|
|
307
564
|
])
|
|
308
565
|
),
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
566
|
+
// The pages of the query on show, up to the page the list stands on; all
|
|
567
|
+
// of them while the latest query has not answered yet.
|
|
568
|
+
types.variableDeclaration('const', [
|
|
569
|
+
types.variableDeclarator(
|
|
570
|
+
types.identifier('last'),
|
|
571
|
+
types.conditionalExpression(
|
|
572
|
+
types.binaryExpression(
|
|
573
|
+
'===',
|
|
574
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
575
|
+
latestOf('sig')
|
|
576
|
+
),
|
|
577
|
+
latestOf('page'),
|
|
578
|
+
types.identifier('Infinity')
|
|
579
|
+
)
|
|
580
|
+
),
|
|
581
|
+
]),
|
|
582
|
+
// This is what DataProvider stores as its data, and so what the list renders.
|
|
312
583
|
types.returnStatement(
|
|
313
584
|
rowsPath ? writeAtPath(payload(), rowsPath, accumulatedRows) : accumulatedRows
|
|
314
585
|
),
|
|
@@ -431,35 +702,103 @@ export const buildSentinelObserverEffect = (
|
|
|
431
702
|
])
|
|
432
703
|
)
|
|
433
704
|
|
|
705
|
+
const styleAttribute = (entries: Record<string, string | number>): types.JSXAttribute =>
|
|
706
|
+
types.jsxAttribute(
|
|
707
|
+
types.jsxIdentifier('style'),
|
|
708
|
+
types.jsxExpressionContainer(
|
|
709
|
+
types.objectExpression(
|
|
710
|
+
Object.entries(entries).map(([key, value]) =>
|
|
711
|
+
types.objectProperty(
|
|
712
|
+
types.identifier(key),
|
|
713
|
+
typeof value === 'number' ? types.numericLiteral(value) : types.stringLiteral(value)
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
)
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
const sentinelTag = (
|
|
721
|
+
tag: string,
|
|
722
|
+
attributes: types.JSXAttribute[],
|
|
723
|
+
children: types.JSXElement[] = []
|
|
724
|
+
): types.JSXElement =>
|
|
725
|
+
types.jsxElement(
|
|
726
|
+
types.jsxOpeningElement(types.jsxIdentifier(tag), attributes, children.length === 0),
|
|
727
|
+
children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(tag)),
|
|
728
|
+
children,
|
|
729
|
+
children.length === 0
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
/** Parents whose content is phrasing only: a `<div>` in one closes it in the parser. */
|
|
733
|
+
const PHRASING_PARENTS: ReadonlySet<string> = new Set([
|
|
734
|
+
'p',
|
|
735
|
+
'span',
|
|
736
|
+
'a',
|
|
737
|
+
'button',
|
|
738
|
+
'label',
|
|
739
|
+
'legend',
|
|
740
|
+
'caption',
|
|
741
|
+
'h1',
|
|
742
|
+
'h2',
|
|
743
|
+
'h3',
|
|
744
|
+
'h4',
|
|
745
|
+
'h5',
|
|
746
|
+
'h6',
|
|
747
|
+
'strong',
|
|
748
|
+
'em',
|
|
749
|
+
'b',
|
|
750
|
+
'i',
|
|
751
|
+
'small',
|
|
752
|
+
])
|
|
753
|
+
|
|
434
754
|
/**
|
|
435
755
|
* `<div ref={ds_N_sentinelRef} aria-hidden="true" style={{ height: '1px' }} />`
|
|
436
756
|
*
|
|
437
757
|
* An invisible one-pixel row after the list. It is generated rather than
|
|
438
758
|
* authored because it is not a control — there is nothing to style or click,
|
|
439
759
|
* only a position in the document for the observer to watch.
|
|
760
|
+
*
|
|
761
|
+
* It takes the element its parent may hold: a table section holds rows, a
|
|
762
|
+
* list holds items, a paragraph holds phrasing. A `<div>` among a table's
|
|
763
|
+
* rows is moved out of the table by the parser, so the server's markup and
|
|
764
|
+
* the client's tree differ and React's hydration fails on every such list.
|
|
440
765
|
*/
|
|
441
|
-
export const buildSentinelElement = (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
),
|
|
450
|
-
types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
|
|
451
|
-
types.jsxAttribute(
|
|
452
|
-
types.jsxIdentifier('style'),
|
|
453
|
-
types.jsxExpressionContainer(
|
|
454
|
-
types.objectExpression([
|
|
455
|
-
types.objectProperty(types.identifier('height'), types.stringLiteral('1px')),
|
|
456
|
-
])
|
|
457
|
-
)
|
|
458
|
-
),
|
|
459
|
-
],
|
|
460
|
-
true
|
|
766
|
+
export const buildSentinelElement = (
|
|
767
|
+
vars: InfiniteScrollVars,
|
|
768
|
+
parentTag: string | null = null
|
|
769
|
+
): types.JSXElement => {
|
|
770
|
+
const observed = [
|
|
771
|
+
types.jsxAttribute(
|
|
772
|
+
types.jsxIdentifier('ref'),
|
|
773
|
+
types.jsxExpressionContainer(types.identifier(vars.sentinelRefVar))
|
|
461
774
|
),
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
775
|
+
types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
|
|
776
|
+
]
|
|
777
|
+
const emptyCell = () => sentinelTag('td', [styleAttribute({ padding: 0, border: 0 })])
|
|
778
|
+
switch (parentTag) {
|
|
779
|
+
case 'tbody':
|
|
780
|
+
case 'thead':
|
|
781
|
+
case 'tfoot':
|
|
782
|
+
return sentinelTag('tr', [...observed, styleAttribute({ height: '1px' })], [emptyCell()])
|
|
783
|
+
case 'table':
|
|
784
|
+
return sentinelTag(
|
|
785
|
+
'tbody',
|
|
786
|
+
[...observed, styleAttribute({ height: '1px' })],
|
|
787
|
+
[sentinelTag('tr', [], [emptyCell()])]
|
|
788
|
+
)
|
|
789
|
+
case 'tr':
|
|
790
|
+
return sentinelTag('td', [...observed, styleAttribute({ height: '1px', padding: 0 })])
|
|
791
|
+
case 'ul':
|
|
792
|
+
case 'ol':
|
|
793
|
+
case 'menu':
|
|
794
|
+
return sentinelTag('li', [...observed, styleAttribute({ height: '1px', listStyle: 'none' })])
|
|
795
|
+
default:
|
|
796
|
+
if (parentTag && PHRASING_PARENTS.has(parentTag)) {
|
|
797
|
+
return sentinelTag('span', [
|
|
798
|
+
...observed,
|
|
799
|
+
styleAttribute({ display: 'block', height: '1px' }),
|
|
800
|
+
])
|
|
801
|
+
}
|
|
802
|
+
return sentinelTag('div', [...observed, styleAttribute({ height: '1px' })])
|
|
803
|
+
}
|
|
804
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PRODUCTS_TABLE_NAME } from './product-price-sort'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a generated list route orders the rows a visitor's sort leaves tied or
|
|
5
|
+
* empty, in the SQL fetchers that serve the platform's listing pages
|
|
6
|
+
* (`teleport.ts`, `postgresql.ts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The platform tables whose listing pages offer visitors a sort — the
|
|
11
|
+
* products page, the blog listing, the booking page — each with an `id` from
|
|
12
|
+
* its first migration.
|
|
13
|
+
*/
|
|
14
|
+
const LISTING_TABLES_WITH_ID: ReadonlySet<string> = new Set([
|
|
15
|
+
PRODUCTS_TABLE_NAME,
|
|
16
|
+
'teleport_blog_posts',
|
|
17
|
+
'teleport_booking_items',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a table is one of those listing tables, so its rows are told apart
|
|
22
|
+
* by `id` even when the project saved no columns for it (see `row-order.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export const isListingTableWithId = (tableName: string): boolean =>
|
|
25
|
+
LISTING_TABLES_WITH_ID.has(tableName)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Appended to every sorted column: a row without a value comes after the
|
|
29
|
+
* rows with one, whichever the direction. PostgreSQL puts NULLs FIRST in a
|
|
30
|
+
* descending order, so "longest read first" opened on the posts without a
|
|
31
|
+
* reading time. Only on the ORDER BY a fetcher tries first — its plain-column
|
|
32
|
+
* fallback leaves it out, for a database that rejects the clause.
|
|
33
|
+
*/
|
|
34
|
+
export const SORT_NULLS_LAST_SQL = ' NULLS LAST'
|