@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.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 (136) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +87 -4
  2. package/__tests__/booking-item-transform.test.ts +120 -0
  3. package/__tests__/browser-row-policy.test.ts +8 -2
  4. package/__tests__/fetchers.test.ts +1 -1
  5. package/__tests__/fixed-list-sort.test.ts +1 -1
  6. package/__tests__/infinite-scroll.test.ts +154 -5
  7. package/__tests__/list-sort-order.test.ts +61 -0
  8. package/__tests__/null-filter-where.test.ts +132 -0
  9. package/__tests__/pg-date-parser.test.ts +73 -0
  10. package/__tests__/plain-list-sort.test.ts +1 -1
  11. package/__tests__/product-price-sort.ts +11 -6
  12. package/__tests__/range-control.test.ts +159 -0
  13. package/__tests__/search-input-marker.test.ts +86 -0
  14. package/__tests__/search-url-sync.test.ts +34 -8
  15. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  16. package/dist/cjs/count-effect.d.ts +7 -1
  17. package/dist/cjs/count-effect.d.ts.map +1 -1
  18. package/dist/cjs/count-effect.js +15 -5
  19. package/dist/cjs/count-effect.js.map +1 -1
  20. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  21. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  22. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  23. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  24. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  25. package/dist/cjs/fetchers/postgresql.js +3 -1
  26. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  27. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  28. package/dist/cjs/fetchers/raw-query.js +2 -1
  29. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  30. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  31. package/dist/cjs/fetchers/teleport.js +4 -2
  32. package/dist/cjs/fetchers/teleport.js.map +1 -1
  33. package/dist/cjs/infinite-scroll.d.ts +25 -2
  34. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  35. package/dist/cjs/infinite-scroll.js +144 -33
  36. package/dist/cjs/infinite-scroll.js.map +1 -1
  37. package/dist/cjs/list-sort-order.d.ts +14 -0
  38. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  39. package/dist/cjs/list-sort-order.js +36 -0
  40. package/dist/cjs/list-sort-order.js.map +1 -0
  41. package/dist/cjs/pagination-controls.d.ts +22 -3
  42. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  43. package/dist/cjs/pagination-controls.js +29 -3
  44. package/dist/cjs/pagination-controls.js.map +1 -1
  45. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  46. package/dist/cjs/pagination-plugin.js +137 -101
  47. package/dist/cjs/pagination-plugin.js.map +1 -1
  48. package/dist/cjs/range-control.d.ts +45 -0
  49. package/dist/cjs/range-control.d.ts.map +1 -0
  50. package/dist/cjs/range-control.js +165 -0
  51. package/dist/cjs/range-control.js.map +1 -0
  52. package/dist/cjs/row-order.d.ts +3 -2
  53. package/dist/cjs/row-order.d.ts.map +1 -1
  54. package/dist/cjs/row-order.js +5 -4
  55. package/dist/cjs/row-order.js.map +1 -1
  56. package/dist/cjs/search-input.d.ts +42 -0
  57. package/dist/cjs/search-input.d.ts.map +1 -0
  58. package/dist/cjs/search-input.js +158 -0
  59. package/dist/cjs/search-input.js.map +1 -0
  60. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  61. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  62. package/dist/cjs/transformations/booking-item.js +27 -0
  63. package/dist/cjs/transformations/booking-item.js.map +1 -0
  64. package/dist/cjs/transformations/index.d.ts +4 -2
  65. package/dist/cjs/transformations/index.d.ts.map +1 -1
  66. package/dist/cjs/transformations/index.js +14 -5
  67. package/dist/cjs/transformations/index.js.map +1 -1
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/esm/count-effect.d.ts +7 -1
  70. package/dist/esm/count-effect.d.ts.map +1 -1
  71. package/dist/esm/count-effect.js +15 -5
  72. package/dist/esm/count-effect.js.map +1 -1
  73. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  74. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  75. package/dist/esm/fetchers/pg-client-import.js +12 -0
  76. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  77. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  78. package/dist/esm/fetchers/postgresql.js +3 -1
  79. package/dist/esm/fetchers/postgresql.js.map +1 -1
  80. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  81. package/dist/esm/fetchers/raw-query.js +2 -1
  82. package/dist/esm/fetchers/raw-query.js.map +1 -1
  83. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  84. package/dist/esm/fetchers/teleport.js +4 -2
  85. package/dist/esm/fetchers/teleport.js.map +1 -1
  86. package/dist/esm/infinite-scroll.d.ts +25 -2
  87. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  88. package/dist/esm/infinite-scroll.js +142 -32
  89. package/dist/esm/infinite-scroll.js.map +1 -1
  90. package/dist/esm/list-sort-order.d.ts +14 -0
  91. package/dist/esm/list-sort-order.d.ts.map +1 -0
  92. package/dist/esm/list-sort-order.js +32 -0
  93. package/dist/esm/list-sort-order.js.map +1 -0
  94. package/dist/esm/pagination-controls.d.ts +22 -3
  95. package/dist/esm/pagination-controls.d.ts.map +1 -1
  96. package/dist/esm/pagination-controls.js +28 -2
  97. package/dist/esm/pagination-controls.js.map +1 -1
  98. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  99. package/dist/esm/pagination-plugin.js +138 -102
  100. package/dist/esm/pagination-plugin.js.map +1 -1
  101. package/dist/esm/range-control.d.ts +45 -0
  102. package/dist/esm/range-control.d.ts.map +1 -0
  103. package/dist/esm/range-control.js +137 -0
  104. package/dist/esm/range-control.js.map +1 -0
  105. package/dist/esm/row-order.d.ts +3 -2
  106. package/dist/esm/row-order.d.ts.map +1 -1
  107. package/dist/esm/row-order.js +5 -4
  108. package/dist/esm/row-order.js.map +1 -1
  109. package/dist/esm/search-input.d.ts +42 -0
  110. package/dist/esm/search-input.d.ts.map +1 -0
  111. package/dist/esm/search-input.js +129 -0
  112. package/dist/esm/search-input.js.map +1 -0
  113. package/dist/esm/transformations/booking-item.d.ts +24 -0
  114. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  115. package/dist/esm/transformations/booking-item.js +23 -0
  116. package/dist/esm/transformations/booking-item.js.map +1 -0
  117. package/dist/esm/transformations/index.d.ts +4 -2
  118. package/dist/esm/transformations/index.d.ts.map +1 -1
  119. package/dist/esm/transformations/index.js +14 -5
  120. package/dist/esm/transformations/index.js.map +1 -1
  121. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +5 -5
  123. package/src/count-effect.ts +31 -8
  124. package/src/fetchers/pg-client-import.ts +16 -0
  125. package/src/fetchers/postgresql.ts +15 -9
  126. package/src/fetchers/raw-query.ts +2 -1
  127. package/src/fetchers/teleport.ts +11 -8
  128. package/src/infinite-scroll.ts +240 -79
  129. package/src/list-sort-order.ts +34 -0
  130. package/src/pagination-controls.ts +26 -3
  131. package/src/pagination-plugin.ts +163 -135
  132. package/src/range-control.ts +207 -0
  133. package/src/row-order.ts +5 -4
  134. package/src/search-input.ts +190 -0
  135. package/src/transformations/booking-item.ts +71 -0
  136. package/src/transformations/index.ts +25 -2
@@ -15,6 +15,15 @@ import * as types from '@babel/types'
15
15
  * and re-enter it. The ref is keyed by a SIGNATURE of every fetch param except
16
16
  * the page, so a filter, sort or search change starts a fresh list instead of
17
17
  * interleaving rows from two different queries.
18
+ *
19
+ * `fetchData` records the query and the page the list stands on BEFORE it
20
+ * requests them (`latest`), and only the answer to that request changes the
21
+ * list. A filter, sort or search change made past page 1 fires the old page
22
+ * under the new query first, then page 1 once the page is reset: kept, the
23
+ * stray page sat in the list, the page counter said 1, and the next "Load
24
+ * more" fetched a page already on show. `DataProvider` also renders whichever
25
+ * answer arrives LAST, so a late answer to an earlier query must not replace
26
+ * the list either.
18
27
  */
19
28
 
20
29
  // tslint:disable:no-any
@@ -62,6 +71,7 @@ export const buildInfiniteScrollDeclarations = (
62
71
  types.objectExpression([
63
72
  types.objectProperty(types.identifier('sig'), types.stringLiteral('')),
64
73
  types.objectProperty(types.identifier('pages'), types.objectExpression([])),
74
+ types.objectProperty(types.identifier('latest'), types.nullLiteral()),
65
75
  ]),
66
76
  ])
67
77
  ),
@@ -161,11 +171,56 @@ const writeAtPath = (
161
171
  ])
162
172
  }
163
173
 
174
+ /** `JSON.stringify({ ...params, page: 0 })`: everything that identifies the query but the page. */
175
+ const buildQuerySignature = (): types.CallExpression =>
176
+ types.callExpression(
177
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
178
+ [
179
+ types.objectExpression([
180
+ types.spreadElement(types.identifier('params')),
181
+ types.objectProperty(types.identifier('page'), types.numericLiteral(0)),
182
+ ]),
183
+ ]
184
+ )
185
+
186
+ /** `Number(params.page) || 1` */
187
+ const buildRequestedPage = (): types.LogicalExpression =>
188
+ types.logicalExpression(
189
+ '||',
190
+ types.callExpression(types.identifier('Number'), [
191
+ types.memberExpression(types.identifier('params'), types.identifier('page')),
192
+ ]),
193
+ types.numericLiteral(1)
194
+ )
195
+
196
+ const accumCurrentOf = (vars: InfiniteScrollVars): types.MemberExpression =>
197
+ types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
198
+
199
+ /**
200
+ * What `fetchData` runs before its request: the query and the page the list
201
+ * now stands on.
202
+ *
203
+ * ds_0_accumRef.current.latest = { sig: JSON.stringify({ ...params, page: 0 }), page: Number(params.page) || 1 }
204
+ */
205
+ export const buildAccumulatingRequestMark = (vars: InfiniteScrollVars): types.Statement =>
206
+ types.expressionStatement(
207
+ types.assignmentExpression(
208
+ '=',
209
+ types.memberExpression(accumCurrentOf(vars), types.identifier('latest')),
210
+ types.objectExpression([
211
+ types.objectProperty(types.identifier('sig'), buildQuerySignature()),
212
+ types.objectProperty(types.identifier('page'), buildRequestedPage()),
213
+ ])
214
+ )
215
+ )
216
+
164
217
  /**
165
218
  * The `.then(...)` tail that accumulates a page and reports whether more exist.
166
219
  *
167
220
  * Returned as the resolved value of `fetchData`, so `DataProvider` stores the
168
- * whole accumulated list as its data.
221
+ * whole accumulated list as its data: the pages of the query on show, first
222
+ * to the page it stands on. An answer to anything but the latest request
223
+ * (`buildAccumulatingRequestMark`) is not kept, and gets the list as it is.
169
224
  *
170
225
  * `itemsPath` is where the rows sit inside a payload that wraps them
171
226
  * (`{ items: [...], total }`): the rows are read there and the accumulated list
@@ -178,10 +233,11 @@ export const buildAccumulatingResponseHandler = (
178
233
  perPage: number,
179
234
  itemsPath?: string[]
180
235
  ): types.ArrowFunctionExpression => {
181
- const accumCurrent = (): types.MemberExpression =>
182
- types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
236
+ const accumCurrent = (): types.MemberExpression => accumCurrentOf(vars)
183
237
  const accumPages = (): types.MemberExpression =>
184
238
  types.memberExpression(accumCurrent(), types.identifier('pages'))
239
+ const latestOf = (key: string): types.MemberExpression =>
240
+ types.memberExpression(types.identifier('latest'), types.identifier(key))
185
241
  const payload = (): types.OptionalMemberExpression =>
186
242
  types.optionalMemberExpression(
187
243
  types.identifier('response'),
@@ -191,6 +247,8 @@ export const buildAccumulatingResponseHandler = (
191
247
  )
192
248
  const rowsPath = itemsPath?.length ? itemsPath : undefined
193
249
 
250
+ // Every page held up to `last`, flattened in page order — a numeric sort, so
251
+ // page 10 does not land between 1 and 2.
194
252
  const accumulatedRows = types.callExpression(
195
253
  types.memberExpression(
196
254
  types.callExpression(
@@ -198,12 +256,23 @@ export const buildAccumulatingResponseHandler = (
198
256
  types.callExpression(
199
257
  types.memberExpression(
200
258
  types.callExpression(
201
- types.memberExpression(types.identifier('Object'), types.identifier('keys')),
202
- [accumPages()]
259
+ types.memberExpression(
260
+ types.callExpression(
261
+ types.memberExpression(types.identifier('Object'), types.identifier('keys')),
262
+ [accumPages()]
263
+ ),
264
+ types.identifier('map')
265
+ ),
266
+ [types.identifier('Number')]
203
267
  ),
204
- types.identifier('map')
268
+ types.identifier('filter')
205
269
  ),
206
- [types.identifier('Number')]
270
+ [
271
+ types.arrowFunctionExpression(
272
+ [types.identifier('p')],
273
+ types.binaryExpression('<=', types.identifier('p'), types.identifier('last'))
274
+ ),
275
+ ]
207
276
  ),
208
277
  types.identifier('sort')
209
278
  ),
@@ -242,73 +311,97 @@ export const buildAccumulatingResponseHandler = (
242
311
  ),
243
312
  ]),
244
313
  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
- ),
314
+ types.variableDeclarator(types.identifier('page'), buildRequestedPage()),
255
315
  ]),
256
316
  // Everything that identifies the QUERY, with the page zeroed out: two
257
317
  // fetches that differ only by page belong to the same accumulated list.
318
+ types.variableDeclaration('const', [
319
+ types.variableDeclarator(types.identifier('sig'), buildQuerySignature()),
320
+ ]),
321
+ // The request the list stands on now, recorded when it was made.
258
322
  types.variableDeclaration('const', [
259
323
  types.variableDeclarator(
260
- types.identifier('sig'),
261
- types.callExpression(
262
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
263
- [
264
- types.objectExpression([
265
- types.spreadElement(types.identifier('params')),
266
- types.objectProperty(types.identifier('page'), types.numericLiteral(0)),
267
- ]),
268
- ]
324
+ types.identifier('latest'),
325
+ types.logicalExpression(
326
+ '||',
327
+ types.memberExpression(accumCurrent(), types.identifier('latest')),
328
+ types.objectExpression([
329
+ types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
330
+ types.objectProperty(types.identifier('page'), types.identifier('page'), false, true),
331
+ ])
269
332
  )
270
333
  ),
271
334
  ]),
335
+ // Only its answer is kept: an earlier query's, or a page past the one a
336
+ // filter / sort / search change restarted the list at, changes nothing.
272
337
  types.ifStatement(
273
- types.binaryExpression(
274
- '!==',
275
- types.memberExpression(accumCurrent(), types.identifier('sig')),
276
- types.identifier('sig')
338
+ types.logicalExpression(
339
+ '&&',
340
+ types.binaryExpression('===', types.identifier('sig'), latestOf('sig')),
341
+ types.binaryExpression('<=', types.identifier('page'), latestOf('page'))
277
342
  ),
278
343
  types.blockStatement([
344
+ types.ifStatement(
345
+ types.binaryExpression(
346
+ '!==',
347
+ types.memberExpression(accumCurrent(), types.identifier('sig')),
348
+ types.identifier('sig')
349
+ ),
350
+ types.blockStatement([
351
+ types.expressionStatement(
352
+ types.assignmentExpression(
353
+ '=',
354
+ types.memberExpression(accumCurrent(), types.identifier('sig')),
355
+ types.identifier('sig')
356
+ )
357
+ ),
358
+ types.expressionStatement(
359
+ types.assignmentExpression('=', accumPages(), types.objectExpression([]))
360
+ ),
361
+ ])
362
+ ),
279
363
  types.expressionStatement(
280
364
  types.assignmentExpression(
281
365
  '=',
282
- accumCurrent(),
283
- types.objectExpression([
284
- types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
285
- types.objectProperty(types.identifier('pages'), types.objectExpression([])),
286
- ])
366
+ types.memberExpression(accumPages(), types.identifier('page'), true),
367
+ types.identifier('rows')
287
368
  )
288
369
  ),
289
- ])
290
- ),
291
- types.expressionStatement(
292
- types.assignmentExpression(
293
- '=',
294
- types.memberExpression(accumPages(), types.identifier('page'), true),
295
- types.identifier('rows')
296
- )
297
- ),
298
- // A page that came back short is the last one. This is the only
299
- // end-of-data signal a data source that cannot count ever gives.
300
- types.expressionStatement(
301
- types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
302
- types.binaryExpression(
303
- '>=',
304
- types.memberExpression(types.identifier('rows'), types.identifier('length')),
305
- types.numericLiteral(perPage)
370
+ // A page that came back short is the last one. This is the only
371
+ // end-of-data signal a data source that cannot count ever gives.
372
+ types.ifStatement(
373
+ types.binaryExpression('===', types.identifier('page'), latestOf('page')),
374
+ types.blockStatement([
375
+ types.expressionStatement(
376
+ types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
377
+ types.binaryExpression(
378
+ '>=',
379
+ types.memberExpression(types.identifier('rows'), types.identifier('length')),
380
+ types.numericLiteral(perPage)
381
+ ),
382
+ ])
383
+ ),
384
+ ])
306
385
  ),
307
386
  ])
308
387
  ),
309
- // Every page held so far, flattened in page order — a numeric sort, so
310
- // page 10 does not land between 1 and 2. This is what DataProvider stores
311
- // as its data, and so what the list renders.
388
+ // The pages of the query on show, up to the page the list stands on; all
389
+ // of them while the latest query has not answered yet.
390
+ types.variableDeclaration('const', [
391
+ types.variableDeclarator(
392
+ types.identifier('last'),
393
+ types.conditionalExpression(
394
+ types.binaryExpression(
395
+ '===',
396
+ types.memberExpression(accumCurrent(), types.identifier('sig')),
397
+ latestOf('sig')
398
+ ),
399
+ latestOf('page'),
400
+ types.identifier('Infinity')
401
+ )
402
+ ),
403
+ ]),
404
+ // This is what DataProvider stores as its data, and so what the list renders.
312
405
  types.returnStatement(
313
406
  rowsPath ? writeAtPath(payload(), rowsPath, accumulatedRows) : accumulatedRows
314
407
  ),
@@ -431,35 +524,103 @@ export const buildSentinelObserverEffect = (
431
524
  ])
432
525
  )
433
526
 
527
+ const styleAttribute = (entries: Record<string, string | number>): types.JSXAttribute =>
528
+ types.jsxAttribute(
529
+ types.jsxIdentifier('style'),
530
+ types.jsxExpressionContainer(
531
+ types.objectExpression(
532
+ Object.entries(entries).map(([key, value]) =>
533
+ types.objectProperty(
534
+ types.identifier(key),
535
+ typeof value === 'number' ? types.numericLiteral(value) : types.stringLiteral(value)
536
+ )
537
+ )
538
+ )
539
+ )
540
+ )
541
+
542
+ const sentinelTag = (
543
+ tag: string,
544
+ attributes: types.JSXAttribute[],
545
+ children: types.JSXElement[] = []
546
+ ): types.JSXElement =>
547
+ types.jsxElement(
548
+ types.jsxOpeningElement(types.jsxIdentifier(tag), attributes, children.length === 0),
549
+ children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(tag)),
550
+ children,
551
+ children.length === 0
552
+ )
553
+
554
+ /** Parents whose content is phrasing only: a `<div>` in one closes it in the parser. */
555
+ const PHRASING_PARENTS: ReadonlySet<string> = new Set([
556
+ 'p',
557
+ 'span',
558
+ 'a',
559
+ 'button',
560
+ 'label',
561
+ 'legend',
562
+ 'caption',
563
+ 'h1',
564
+ 'h2',
565
+ 'h3',
566
+ 'h4',
567
+ 'h5',
568
+ 'h6',
569
+ 'strong',
570
+ 'em',
571
+ 'b',
572
+ 'i',
573
+ 'small',
574
+ ])
575
+
434
576
  /**
435
577
  * `<div ref={ds_N_sentinelRef} aria-hidden="true" style={{ height: '1px' }} />`
436
578
  *
437
579
  * An invisible one-pixel row after the list. It is generated rather than
438
580
  * authored because it is not a control — there is nothing to style or click,
439
581
  * only a position in the document for the observer to watch.
582
+ *
583
+ * It takes the element its parent may hold: a table section holds rows, a
584
+ * list holds items, a paragraph holds phrasing. A `<div>` among a table's
585
+ * rows is moved out of the table by the parser, so the server's markup and
586
+ * the client's tree differ and React's hydration fails on every such list.
440
587
  */
441
- export const buildSentinelElement = (vars: InfiniteScrollVars): types.JSXElement =>
442
- types.jsxElement(
443
- types.jsxOpeningElement(
444
- types.jsxIdentifier('div'),
445
- [
446
- types.jsxAttribute(
447
- types.jsxIdentifier('ref'),
448
- types.jsxExpressionContainer(types.identifier(vars.sentinelRefVar))
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
588
+ export const buildSentinelElement = (
589
+ vars: InfiniteScrollVars,
590
+ parentTag: string | null = null
591
+ ): types.JSXElement => {
592
+ const observed = [
593
+ types.jsxAttribute(
594
+ types.jsxIdentifier('ref'),
595
+ types.jsxExpressionContainer(types.identifier(vars.sentinelRefVar))
461
596
  ),
462
- null,
463
- [],
464
- true
465
- )
597
+ types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
598
+ ]
599
+ const emptyCell = () => sentinelTag('td', [styleAttribute({ padding: 0, border: 0 })])
600
+ switch (parentTag) {
601
+ case 'tbody':
602
+ case 'thead':
603
+ case 'tfoot':
604
+ return sentinelTag('tr', [...observed, styleAttribute({ height: '1px' })], [emptyCell()])
605
+ case 'table':
606
+ return sentinelTag(
607
+ 'tbody',
608
+ [...observed, styleAttribute({ height: '1px' })],
609
+ [sentinelTag('tr', [], [emptyCell()])]
610
+ )
611
+ case 'tr':
612
+ return sentinelTag('td', [...observed, styleAttribute({ height: '1px', padding: 0 })])
613
+ case 'ul':
614
+ case 'ol':
615
+ case 'menu':
616
+ return sentinelTag('li', [...observed, styleAttribute({ height: '1px', listStyle: 'none' })])
617
+ default:
618
+ if (parentTag && PHRASING_PARENTS.has(parentTag)) {
619
+ return sentinelTag('span', [
620
+ ...observed,
621
+ styleAttribute({ display: 'block', height: '1px' }),
622
+ ])
623
+ }
624
+ return sentinelTag('div', [...observed, styleAttribute({ height: '1px' })])
625
+ }
626
+ }
@@ -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'
@@ -17,7 +17,9 @@ export const PAGINATION_CONTROL_ATTR = 'data-tq-pagination-control'
17
17
  * `'pages'` is the container the page-number buttons are repeated into and
18
18
  * `'page'` the single template button inside it. `'ellipsis'` and `'sentinel'`
19
19
  * are reserved for an authored gap marker / scroll sentinel; until a builder
20
- * emits them both runtimes fall back to generating their own.
20
+ * emits them both runtimes fall back to generating their own. `'range'` is a
21
+ * TEXT element ("Showing 1–8 of 12") both runtimes fill with the live row
22
+ * range — see `range-control.ts`.
21
23
  */
22
24
  export type PaginationControlKind =
23
25
  | 'first'
@@ -29,6 +31,26 @@ export type PaginationControlKind =
29
31
  | 'last'
30
32
  | 'load-more'
31
33
  | 'sentinel'
34
+ | 'range'
35
+
36
+ /**
37
+ * Which mapper a `range` control belongs to, when it does not sit inside that
38
+ * mapper's pagination container: the array mapper's editor node id, which the
39
+ * UIDL carries as `nodeId` on the `cms-list-repeater`. A template's footer
40
+ * ("Showing 1–8 of 12") is laid out by the author, not inside a widget, so the
41
+ * container-ownership rule the buttons use cannot reach it.
42
+ *
43
+ * ⛔ Both attribute names must stay identical to the editor's
44
+ * (`array-mapper-pagination-settings.ts`) and the canvas renderer's
45
+ * (`array-mapper-pagination.ts`) copies.
46
+ */
47
+ export const PAGINATION_FOR_ATTR = 'data-tq-pagination-for'
48
+
49
+ /** The words of a `range` control, with `{from}`, `{to}` and `{total}` placeholders. */
50
+ export const RANGE_TEMPLATE_ATTR = 'data-tq-range-template'
51
+
52
+ /** What a `range` control reads when it carries no template. */
53
+ export const DEFAULT_RANGE_TEMPLATE = '{from}–{to} of {total}'
32
54
 
33
55
  // tslint:disable:no-any
34
56
 
@@ -152,13 +174,14 @@ export function removeJSXAttribute(node: any, attrName: string): void {
152
174
  *
153
175
  * Children carrying NO marker are always kept: they are the author's own
154
176
  * additions (a page counter, a spacer) and the generator has no business
155
- * deciding whether they belong.
177
+ * deciding whether they belong. A `range` text is kept in every mode too: the
178
+ * row range is meaningful whether the list pages or appends.
156
179
  */
157
180
  export function dropInactivePaginationControls(
158
181
  paginationNode: any,
159
182
  activeKinds: PaginationControlKind[]
160
183
  ): void {
161
- const active = new Set<string>(activeKinds)
184
+ const active = new Set<string>([...activeKinds, 'range'])
162
185
  const prune = (node: any): void => {
163
186
  if (!node || !Array.isArray(node.children)) {
164
187
  return