@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.
Files changed (199) hide show
  1. package/__tests__/_helpers/first-page-runtime.ts +184 -0
  2. package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
  3. package/__tests__/booking-item-transform.test.ts +120 -0
  4. package/__tests__/browser-row-policy.test.ts +210 -4
  5. package/__tests__/data-source-request-locale.test.ts +4 -3
  6. package/__tests__/fetchers.test.ts +7 -3
  7. package/__tests__/fixed-list-sort.test.ts +1 -1
  8. package/__tests__/infinite-scroll.test.ts +363 -9
  9. package/__tests__/list-sort-order.test.ts +61 -0
  10. package/__tests__/null-filter-where.test.ts +132 -0
  11. package/__tests__/page-data-visitor-read.test.ts +196 -0
  12. package/__tests__/pg-date-parser.test.ts +73 -0
  13. package/__tests__/plain-list-sort.test.ts +2 -2
  14. package/__tests__/plain-list-starting-sort.test.ts +323 -0
  15. package/__tests__/product-price-sort.ts +11 -6
  16. package/__tests__/range-control.test.ts +159 -0
  17. package/__tests__/refetch-loading-state.test.ts +158 -2
  18. package/__tests__/search-input-marker.test.ts +86 -0
  19. package/__tests__/search-url-sync.test.ts +34 -8
  20. package/__tests__/starting-sort-first-page.test.ts +285 -0
  21. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  22. package/dist/cjs/cache/ast.d.ts +2 -0
  23. package/dist/cjs/cache/ast.d.ts.map +1 -1
  24. package/dist/cjs/cache/ast.js +5 -2
  25. package/dist/cjs/cache/ast.js.map +1 -1
  26. package/dist/cjs/count-effect.d.ts +7 -1
  27. package/dist/cjs/count-effect.d.ts.map +1 -1
  28. package/dist/cjs/count-effect.js +15 -5
  29. package/dist/cjs/count-effect.js.map +1 -1
  30. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  31. package/dist/cjs/data-source-fetchers.js +10 -2
  32. package/dist/cjs/data-source-fetchers.js.map +1 -1
  33. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  34. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  35. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  36. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +3 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/raw-query.js +2 -1
  42. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  43. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/teleport.js +4 -2
  45. package/dist/cjs/fetchers/teleport.js.map +1 -1
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +14 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/infinite-scroll.d.ts +89 -4
  53. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  54. package/dist/cjs/infinite-scroll.js +243 -33
  55. package/dist/cjs/infinite-scroll.js.map +1 -1
  56. package/dist/cjs/list-sort-order.d.ts +14 -0
  57. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  58. package/dist/cjs/list-sort-order.js +36 -0
  59. package/dist/cjs/list-sort-order.js.map +1 -0
  60. package/dist/cjs/loading-state.d.ts +23 -2
  61. package/dist/cjs/loading-state.d.ts.map +1 -1
  62. package/dist/cjs/loading-state.js +50 -15
  63. package/dist/cjs/loading-state.js.map +1 -1
  64. package/dist/cjs/pagination-controls.d.ts +22 -3
  65. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  66. package/dist/cjs/pagination-controls.js +29 -3
  67. package/dist/cjs/pagination-controls.js.map +1 -1
  68. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  69. package/dist/cjs/pagination-plugin.js +336 -164
  70. package/dist/cjs/pagination-plugin.js.map +1 -1
  71. package/dist/cjs/range-control.d.ts +45 -0
  72. package/dist/cjs/range-control.d.ts.map +1 -0
  73. package/dist/cjs/range-control.js +165 -0
  74. package/dist/cjs/range-control.js.map +1 -0
  75. package/dist/cjs/row-order.d.ts +3 -2
  76. package/dist/cjs/row-order.d.ts.map +1 -1
  77. package/dist/cjs/row-order.js +5 -4
  78. package/dist/cjs/row-order.js.map +1 -1
  79. package/dist/cjs/search-input.d.ts +42 -0
  80. package/dist/cjs/search-input.d.ts.map +1 -0
  81. package/dist/cjs/search-input.js +158 -0
  82. package/dist/cjs/search-input.js.map +1 -0
  83. package/dist/cjs/sort-utils.d.ts +53 -1
  84. package/dist/cjs/sort-utils.d.ts.map +1 -1
  85. package/dist/cjs/sort-utils.js +105 -13
  86. package/dist/cjs/sort-utils.js.map +1 -1
  87. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  88. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  89. package/dist/cjs/transformations/booking-item.js +27 -0
  90. package/dist/cjs/transformations/booking-item.js.map +1 -0
  91. package/dist/cjs/transformations/index.d.ts +4 -2
  92. package/dist/cjs/transformations/index.d.ts.map +1 -1
  93. package/dist/cjs/transformations/index.js +14 -5
  94. package/dist/cjs/transformations/index.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts +3 -2
  97. package/dist/cjs/utils.d.ts.map +1 -1
  98. package/dist/cjs/utils.js +48 -18
  99. package/dist/cjs/utils.js.map +1 -1
  100. package/dist/esm/cache/ast.d.ts +2 -0
  101. package/dist/esm/cache/ast.d.ts.map +1 -1
  102. package/dist/esm/cache/ast.js +3 -1
  103. package/dist/esm/cache/ast.js.map +1 -1
  104. package/dist/esm/count-effect.d.ts +7 -1
  105. package/dist/esm/count-effect.d.ts.map +1 -1
  106. package/dist/esm/count-effect.js +15 -5
  107. package/dist/esm/count-effect.js.map +1 -1
  108. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  109. package/dist/esm/data-source-fetchers.js +10 -2
  110. package/dist/esm/data-source-fetchers.js.map +1 -1
  111. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  112. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  113. package/dist/esm/fetchers/pg-client-import.js +12 -0
  114. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  115. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  116. package/dist/esm/fetchers/postgresql.js +3 -1
  117. package/dist/esm/fetchers/postgresql.js.map +1 -1
  118. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  119. package/dist/esm/fetchers/raw-query.js +2 -1
  120. package/dist/esm/fetchers/raw-query.js.map +1 -1
  121. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  122. package/dist/esm/fetchers/teleport.js +4 -2
  123. package/dist/esm/fetchers/teleport.js.map +1 -1
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  127. package/dist/esm/index.d.ts.map +1 -1
  128. package/dist/esm/index.js +14 -2
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/infinite-scroll.d.ts +89 -4
  131. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  132. package/dist/esm/infinite-scroll.js +237 -32
  133. package/dist/esm/infinite-scroll.js.map +1 -1
  134. package/dist/esm/list-sort-order.d.ts +14 -0
  135. package/dist/esm/list-sort-order.d.ts.map +1 -0
  136. package/dist/esm/list-sort-order.js +32 -0
  137. package/dist/esm/list-sort-order.js.map +1 -0
  138. package/dist/esm/loading-state.d.ts +23 -2
  139. package/dist/esm/loading-state.d.ts.map +1 -1
  140. package/dist/esm/loading-state.js +50 -15
  141. package/dist/esm/loading-state.js.map +1 -1
  142. package/dist/esm/pagination-controls.d.ts +22 -3
  143. package/dist/esm/pagination-controls.d.ts.map +1 -1
  144. package/dist/esm/pagination-controls.js +28 -2
  145. package/dist/esm/pagination-controls.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +340 -168
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/range-control.d.ts +45 -0
  150. package/dist/esm/range-control.d.ts.map +1 -0
  151. package/dist/esm/range-control.js +137 -0
  152. package/dist/esm/range-control.js.map +1 -0
  153. package/dist/esm/row-order.d.ts +3 -2
  154. package/dist/esm/row-order.d.ts.map +1 -1
  155. package/dist/esm/row-order.js +5 -4
  156. package/dist/esm/row-order.js.map +1 -1
  157. package/dist/esm/search-input.d.ts +42 -0
  158. package/dist/esm/search-input.d.ts.map +1 -0
  159. package/dist/esm/search-input.js +129 -0
  160. package/dist/esm/search-input.js.map +1 -0
  161. package/dist/esm/sort-utils.d.ts +53 -1
  162. package/dist/esm/sort-utils.d.ts.map +1 -1
  163. package/dist/esm/sort-utils.js +100 -12
  164. package/dist/esm/sort-utils.js.map +1 -1
  165. package/dist/esm/transformations/booking-item.d.ts +24 -0
  166. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  167. package/dist/esm/transformations/booking-item.js +23 -0
  168. package/dist/esm/transformations/booking-item.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +4 -2
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +14 -5
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  174. package/dist/esm/utils.d.ts +3 -2
  175. package/dist/esm/utils.d.ts.map +1 -1
  176. package/dist/esm/utils.js +49 -19
  177. package/dist/esm/utils.js.map +1 -1
  178. package/package.json +5 -5
  179. package/src/cache/ast.ts +4 -1
  180. package/src/count-effect.ts +31 -8
  181. package/src/data-source-fetchers.ts +33 -14
  182. package/src/fetchers/pg-client-import.ts +16 -0
  183. package/src/fetchers/postgresql.ts +15 -9
  184. package/src/fetchers/raw-query.ts +2 -1
  185. package/src/fetchers/teleport.ts +19 -10
  186. package/src/fetchers/utils/browser-row-policy.ts +79 -5
  187. package/src/index.ts +8 -2
  188. package/src/infinite-scroll.ts +420 -81
  189. package/src/list-sort-order.ts +34 -0
  190. package/src/loading-state.ts +85 -13
  191. package/src/pagination-controls.ts +26 -3
  192. package/src/pagination-plugin.ts +532 -218
  193. package/src/range-control.ts +207 -0
  194. package/src/row-order.ts +5 -4
  195. package/src/search-input.ts +190 -0
  196. package/src/sort-utils.ts +164 -21
  197. package/src/transformations/booking-item.ts +71 -0
  198. package/src/transformations/index.ts +25 -2
  199. package/src/utils.ts +55 -18
@@ -3,15 +3,28 @@ import {
3
3
  ComponentPluginFactory,
4
4
  ChunkType,
5
5
  FileType,
6
+ UIDLStateDefinition,
6
7
  } from '@teleporthq/teleport-types'
7
8
  import * as types from '@babel/types'
8
9
  import { parseExpression } from '@babel/parser'
9
10
  import { ASTStatementOrder, GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
10
- import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
11
+ import {
12
+ ASTUtils,
13
+ RouteUtils,
14
+ URLQueryWriter,
15
+ URLSearchParamSync,
16
+ } from '@teleporthq/teleport-plugin-common'
11
17
  import { collectFilterLeafConditions, generateSafeFileName, pruneFilterTree } from './utils'
12
18
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
19
  import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
14
- import { appendSortsParam, DynamicSortAST, extractDynamicSort, readFixedSort } from './sort-utils'
20
+ import {
21
+ appendSortsParam,
22
+ buildSortAtStartGuard,
23
+ buildStartingSortsValue,
24
+ DynamicSortAST,
25
+ readRepeaterSort,
26
+ type SortStateDefault,
27
+ } from './sort-utils'
15
28
  import {
16
29
  appendFiltersParam,
17
30
  buildStaticFiltersStringifyCall,
@@ -32,15 +45,21 @@ import {
32
45
  buildCacheStoreThen,
33
46
  buildCachedFetchBody,
34
47
  buildCachedParamsDeclarations,
48
+ paramsVarFor,
35
49
  registerCacheClientImports,
36
50
  } from './cache/ast'
37
51
  import {
38
52
  convertControlToButton,
39
53
  dropInactivePaginationControls,
54
+ findAllPaginationControls,
40
55
  findPaginationControl,
56
+ PAGINATION_FOR_ATTR,
57
+ readJSXAttributeString,
41
58
  setJSXExpressionAttribute,
42
59
  type PaginationControlKind,
43
60
  } from './pagination-controls'
61
+ import { findRangeControls, wireRangeControl } from './range-control'
62
+ import { findAllSearchInputsInJSX, pairSearchInputs, wireSearchInput } from './search-input'
44
63
  import { buildCountFetchEffect, buildCountSeqDeclaration } from './count-effect'
45
64
  import {
46
65
  findAllSearchClearButtonsInJSX,
@@ -53,10 +72,15 @@ import {
53
72
  type NumberedPaginationVars,
54
73
  } from './numbered-pagination'
55
74
  import {
75
+ buildAccumulatingRequestMark,
56
76
  buildAccumulatingResponseHandler,
77
+ buildAppendingRequestCheck,
78
+ buildFirstPageDeclaration,
79
+ buildFirstPageSeedEffect,
57
80
  buildInfiniteScrollDeclarations,
58
81
  buildSentinelElement,
59
82
  buildSentinelObserverEffect,
83
+ buildServerRowsExpression,
60
84
  getInfiniteScrollVars,
61
85
  } from './infinite-scroll'
62
86
  import {
@@ -154,6 +178,10 @@ interface DataSourceUsage {
154
178
  dataSourceIdentifier: string
155
179
  // The cms-list-repeater renderPropIdentifier (e.g., "context_1i871")
156
180
  arrayMapperRenderProp: string
181
+ // The array mapper's editor node id (`nodeId` on the cms-list-repeater), which
182
+ // a `range` control placed outside the pagination container names in its
183
+ // `data-tq-pagination-for` attribute. Absent from UIDL exported before it.
184
+ nodeId?: string
157
185
  // Resource definition from UIDL
158
186
  resourceDefinition: {
159
187
  dataSourceId: string
@@ -187,6 +215,12 @@ interface DataSourceUsage {
187
215
  // Dynamic single-column sort bound to component state (set when legacy sorts empty
188
216
  // and cms-list-repeater declares sort/sortDirection fields)
189
217
  dynamicSort?: DynamicSortAST
218
+ // With `dynamicSort`: the states it reads, at the values a visitor starts
219
+ // with (`readSortStateDefaults`). The build fetches the first page by that
220
+ // sort, and the list takes those rows while the sort is still there. Absent
221
+ // when the build cannot know the starting sort: the list then fetches its
222
+ // first page in the browser.
223
+ startingSort?: SortStateDefault[]
190
224
  // Filters from resource params, as the TREE the inspector authored: condition
191
225
  // leaves and `{ type: 'group', operator, children }` entries (see
192
226
  // `readFilterTree`). Anything that needs the leaves walks the tree with
@@ -446,7 +480,10 @@ function buildFilterDestinationExpression(destination: unknown): types.Expressio
446
480
  }
447
481
 
448
482
  // Scan UIDL to find all data source usages and build a registry
449
- function buildStateRegistry(uidlNode: any): StateRegistry {
483
+ function buildStateRegistry(
484
+ uidlNode: any,
485
+ stateDefinitions?: Record<string, UIDLStateDefinition>
486
+ ): StateRegistry {
450
487
  const usages: DataSourceUsage[] = []
451
488
  const byDataSourceId = new Map<string, DataSourceUsage[]>()
452
489
  const byArrayMapperRenderProp = new Map<string, DataSourceUsage>()
@@ -515,15 +552,20 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
515
552
  // is skipped for a dynamic sort) — the pre-rendered HTML carried no
516
553
  // posts, and the browser fetched page 1 a second time. Only a sort
517
554
  // bound to state (the admin panel's sortable columns) is dynamic.
518
- let dynamicSort: DynamicSortAST | undefined
519
- if ((!sorts || sorts.length === 0) && content.sort) {
520
- const fixedSort = readFixedSort(content.sort, content.sortDirection)
521
- if (fixedSort) {
522
- sorts = [fixedSort]
523
- } else {
524
- dynamicSort = extractDynamicSort(content.sort, content.sortDirection)
525
- }
555
+ //
556
+ // A sort the visitor picks still starts somewhere — the store's sort
557
+ // dropdown at "Name A-Z". Known at build time, it orders the
558
+ // server-side first page, which the list then shows while the visitor
559
+ // leaves the sort alone. Read as a plain list's page fetch reads it.
560
+ const repeaterSort =
561
+ !sorts || sorts.length === 0
562
+ ? readRepeaterSort(content.sort, content.sortDirection, stateDefinitions)
563
+ : {}
564
+ if (repeaterSort.fixed) {
565
+ sorts = [repeaterSort.fixed]
526
566
  }
567
+ const dynamicSort = repeaterSort.dynamic
568
+ const startingSort = repeaterSort.starting
527
569
 
528
570
  // Extract filters from parent's resource params, keeping the group tree
529
571
  // the inspector authored (see `readFilterTree`).
@@ -603,6 +645,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
603
645
  index: index++,
604
646
  dataSourceIdentifier: parentDataSource.identifier,
605
647
  arrayMapperRenderProp,
648
+ nodeId: typeof content.nodeId === 'string' ? content.nodeId : undefined,
606
649
  resourceDefinition: {
607
650
  dataSourceId: parentDataSource.resourceDef.dataSourceId || '',
608
651
  tableName: parentDataSource.resourceDef.tableName || '',
@@ -617,6 +660,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
617
660
  queryColumns,
618
661
  sorts,
619
662
  dynamicSort,
663
+ startingSort,
620
664
  filters,
621
665
  filterStateIds,
622
666
  filterPropIds,
@@ -704,6 +748,9 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
704
748
  setPageStateVar: string
705
749
  maxPagesStateVar: string
706
750
  setMaxPagesStateVar: string
751
+ /** The counted row total behind a `range` control; `null` until counted. */
752
+ totalStateVar: string
753
+ setTotalStateVar: string
707
754
  searchQueryVar: string
708
755
  setSearchQueryVar: string
709
756
  debouncedSearchQueryVar: string
@@ -722,6 +769,8 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
722
769
  setPageStateVar: `setDs_${idx}_page`,
723
770
  maxPagesStateVar: `ds_${idx}_maxPages`,
724
771
  setMaxPagesStateVar: `setDs_${idx}_maxPages`,
772
+ totalStateVar: `ds_${idx}_total`,
773
+ setTotalStateVar: `setDs_${idx}_total`,
725
774
  searchQueryVar: `ds_${idx}_searchQuery`,
726
775
  setSearchQueryVar: `setDs_${idx}_searchQuery`,
727
776
  debouncedSearchQueryVar: `ds_${idx}_debouncedQuery`,
@@ -829,7 +878,8 @@ function pushSearchUrlSyncEffects(
829
878
  )
830
879
  // read-back: `?<searchUrlParamKey>=` → the input AND the applied query at once
831
880
  // (`ds_N_adoptUrlSearch`), so an address is not a typed change: it skips the
832
- // debounce and keeps the page — `?searchKeyword=x&page=3` lands on page 3.
881
+ // debounce and keeps a page the address carries — `?searchKeyword=x&page=3`
882
+ // lands on page 3.
833
883
  effectStatements.push(
834
884
  URLSearchParamSync.buildUrlReadBackEffect(
835
885
  usage.searchUrlParamKey,
@@ -842,19 +892,26 @@ function pushSearchUrlSyncEffects(
842
892
 
843
893
  // `const ds_N_adoptUrlSearch = (update) => { … }`: the setter the search's URL
844
894
  // read-back calls with its `(prev) => next` updater. It moves the input and the
845
- // applied (debounced) query together; the combined paginated state keeps its
846
- // page, which a typed change resets to 1 in the debounce effect.
895
+ // applied (debounced) query together. A list whose page is in the address
896
+ // keeps its page — the page sync sets it from the same address. Any other
897
+ // list starts the new query on page 1, as a typed change does in the debounce
898
+ // effect: an appending list has usually advanced past page 1 by the time the
899
+ // router is ready, and the query's first answer would otherwise be its page 2
900
+ // — empty for a query only a few rows match.
847
901
  function buildUrlSearchAdopter(
848
902
  adopterVar: string,
849
903
  usage: DataSourceUsage,
850
904
  vars: ReturnType<typeof getStateVarsForUsage>
851
905
  ): types.VariableDeclaration {
852
906
  const update = types.identifier('update')
907
+ const pageReset = usageHasPageUrlSync(usage)
908
+ ? []
909
+ : [types.objectProperty(types.identifier('page'), types.numericLiteral(1))]
853
910
  const applyToDebounced =
854
911
  usage.category === 'paginated+search'
855
912
  ? // setDs_N_state((state) => {
856
913
  // const next = update(state.debouncedQuery)
857
- // return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next }
914
+ // return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next }
858
915
  // })
859
916
  types.callExpression(types.identifier(vars.setCombinedStateVar), [
860
917
  types.arrowFunctionExpression(
@@ -884,6 +941,7 @@ function buildUrlSearchAdopter(
884
941
  types.identifier('state'),
885
942
  types.objectExpression([
886
943
  types.spreadElement(types.identifier('state')),
944
+ ...pageReset,
887
945
  types.objectProperty(
888
946
  types.identifier('debouncedQuery'),
889
947
  types.identifier('next')
@@ -1186,8 +1244,16 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1186
1244
  }
1187
1245
  const blockStatement = arrowFunction.body as types.BlockStatement
1188
1246
 
1247
+ // The "Showing 1–8 of 12" texts. Known up front because a component that
1248
+ // draws one needs the counted TOTAL in state — `ds_N_maxPages` alone cannot
1249
+ // say how many rows there are — and that state, with the live count that
1250
+ // fills it, is declared per usage below. A component without one emits
1251
+ // exactly what it always did.
1252
+ const rangeControls = findRangeControls(blockStatement)
1253
+ const hasRangeControl = rangeControls.length > 0
1254
+
1189
1255
  // STEP 1: Build state registry from UIDL
1190
- const registry = buildStateRegistry(uidl.node)
1256
+ const registry = buildStateRegistry(uidl.node, uidl.stateDefinitions)
1191
1257
 
1192
1258
  if (registry.usages.length === 0) {
1193
1259
  return structure
@@ -1500,12 +1566,16 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1500
1566
  pushPropIdsAsDeps(countEffectDeps, countEffectSeen, usage.filterPropIds)
1501
1567
  // Same goes for URL-driven filters (already documented above).
1502
1568
  pushUrlSearchParamMemoDeps(countEffectDeps, usage)
1569
+ if (hasRangeControl) {
1570
+ stateDeclarations.push(buildTotalDeclaration(vars))
1571
+ }
1503
1572
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar))
1504
1573
  effectStatements.push(
1505
1574
  buildCountFetchEffect({
1506
1575
  fileName,
1507
1576
  perPage: usage.perPage,
1508
1577
  setMaxPagesVar: vars.setMaxPagesStateVar,
1578
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
1509
1579
  urlParams,
1510
1580
  deps: countEffectDeps,
1511
1581
  countSeqRefVar: vars.countSeqRefVar,
@@ -1572,8 +1642,10 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1572
1642
  // "Next" button, but not to label a numbered strip or to know when an
1573
1643
  // infinite list has reached its end, so those two modes fetch a live
1574
1644
  // count as well.
1645
+ // A range text needs the live count too: the build-time count only
1646
+ // ever became a page count, never the total it shows.
1575
1647
  const needsLiveCount =
1576
- !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll
1648
+ !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll || hasRangeControl
1577
1649
  if (needsLiveCount) {
1578
1650
  const fileName = generateSafeFileName(
1579
1651
  usage.resourceDefinition.dataSourceType,
@@ -1610,12 +1682,16 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1610
1682
  pushPropIdsAsDeps(countDeps, countDepsSeen, usage.filterPropIds)
1611
1683
  pushUrlSearchParamMemoDeps(countDeps, usage)
1612
1684
 
1685
+ if (hasRangeControl) {
1686
+ stateDeclarations.push(buildTotalDeclaration(vars))
1687
+ }
1613
1688
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar))
1614
1689
  effectStatements.push(
1615
1690
  buildCountFetchEffect({
1616
1691
  fileName,
1617
1692
  perPage: usage.perPage,
1618
1693
  setMaxPagesVar: vars.setMaxPagesStateVar,
1694
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
1619
1695
  urlParams: countUrlParams,
1620
1696
  deps: countDeps,
1621
1697
  countSeqRefVar: vars.countSeqRefVar,
@@ -1804,6 +1880,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1804
1880
  // widgets can be matched to their owner rather than counted off in order.
1805
1881
  const usageByProviderNode = new Map<any, DataSourceUsage>()
1806
1882
  const cacheDeclarations: types.Statement[] = []
1883
+ // Only a page has rows of its own to start an appending list from: its
1884
+ // `getStaticProps` fetched page 1 (`updateGetStaticProps`).
1885
+ const firstPageDeclarations: types.Statement[] | undefined = isPage ? [] : undefined
1807
1886
 
1808
1887
  dataProvidersWithRepeaters.forEach((dp) => {
1809
1888
  const nameAttr = dp.openingElement.attributes.find(
@@ -1842,10 +1921,19 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1842
1921
  vars,
1843
1922
  fileName,
1844
1923
  cacheDeclarations,
1845
- localized
1924
+ localized,
1925
+ firstPageDeclarations
1846
1926
  )
1847
1927
  } else if (usage.category === 'paginated-only') {
1848
- updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDeclarations, localized)
1928
+ updateDataProviderForPaginationOnly(
1929
+ dp,
1930
+ usage,
1931
+ vars,
1932
+ fileName,
1933
+ cacheDeclarations,
1934
+ localized,
1935
+ firstPageDeclarations
1936
+ )
1849
1937
  } else if (usage.category === 'search-only') {
1850
1938
  updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclarations, localized)
1851
1939
  } else if (usage.category === 'plain') {
@@ -1856,9 +1944,17 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1856
1944
  // change refetches, instead of leaving the previous rows on screen with
1857
1945
  // no feedback. Runs last so it sees the `fetchData` and
1858
1946
  // `persistDataDuringLoading` attributes the updaters above just wrote.
1947
+ // An appending list keeps its rows on screen while its next page loads;
1948
+ // only a list loading afresh falls through to the loading slot.
1859
1949
  const loadingVars = getLoadingStateVars(usage.index)
1860
- if (applyLoadingStateToDataProvider(dp, loadingVars)) {
1861
- loadingStateDeclarations.push(...buildLoadingStateDeclarations(loadingVars))
1950
+ const appendingRequest =
1951
+ usage.infiniteScroll && usage.paginated
1952
+ ? buildAppendingRequestCheck(getInfiniteScrollVars(usage.index))
1953
+ : undefined
1954
+ if (applyLoadingStateToDataProvider(dp, loadingVars, appendingRequest)) {
1955
+ loadingStateDeclarations.push(
1956
+ ...buildLoadingStateDeclarations(loadingVars, !!appendingRequest)
1957
+ )
1862
1958
  // Recorded because the loading state is only declared for a provider
1863
1959
  // that HAS a loading slot; a Load More button wired later may only
1864
1960
  // reference `ds_N_isFetching` if it actually exists.
@@ -1868,10 +1964,8 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1868
1964
  // An auto-loading list needs something at the end of the rows for its
1869
1965
  // IntersectionObserver to watch.
1870
1966
  if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
1871
- insertSentinelAfterProvider(
1872
- blockStatement,
1873
- dp,
1874
- buildSentinelElement(getInfiniteScrollVars(usage.index))
1967
+ insertSentinelAfterProvider(blockStatement, dp, (parentTag) =>
1968
+ buildSentinelElement(getInfiniteScrollVars(usage.index), parentTag)
1875
1969
  )
1876
1970
  }
1877
1971
 
@@ -1928,6 +2022,12 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1928
2022
  registerCacheClientImports(dependencies, uidl.outputOptions?.folderPath)
1929
2023
  }
1930
2024
 
2025
+ // After the cache declarations: a cached list's first page reads the
2026
+ // `ds_N_params` they hoisted.
2027
+ if (firstPageDeclarations?.length) {
2028
+ ASTStatementOrder.insertStatementsAfterDependencies(blockStatement, firstPageDeclarations)
2029
+ }
2030
+
1931
2031
  // STEP 3.5: Handle DataProviders WITHOUT repeaters (data-source-item type)
1932
2032
  // These access single items like data[0].name and should not re-render on state changes
1933
2033
  // We wrap their params in useMemo to prevent reference changes from triggering re-renders
@@ -1940,25 +2040,17 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1940
2040
  stabilizeDataProviderWithoutRepeater(dp)
1941
2041
  })
1942
2042
 
1943
- // STEP 4: Wire search inputs
1944
- // Match search inputs to usages by order (within each dataSourceIdentifier that has search enabled)
2043
+ // STEP 4: Wire search inputs — a platform input by its place among the
2044
+ // search-enabled mappers, a page's own marked input by the mapper it names
2045
+ // (`search-input.ts`).
1945
2046
  const searchInputs = findAllSearchInputsInJSX(blockStatement)
1946
-
1947
- // Get all search-enabled usages in order
1948
- const searchEnabledUsages = registry.usages.filter((u) => u.searchEnabled)
1949
-
1950
- // Match by order - search input 0 -> searchEnabledUsages[0], etc.
1951
- const usageBySearchInput = new Map<any, DataSourceUsage>()
1952
- searchInputs.forEach((input, idx) => {
1953
- if (idx >= searchEnabledUsages.length) {
1954
- return
2047
+ const usageBySearchInput = pairSearchInputs(searchInputs, registry.usages)
2048
+ for (const input of searchInputs) {
2049
+ const usage = usageBySearchInput.get(input.node)
2050
+ if (usage) {
2051
+ wireSearchInput(input, getStateVarsForUsage(usage))
1955
2052
  }
1956
-
1957
- const usage = searchEnabledUsages[idx]
1958
- const vars = getStateVarsForUsage(usage)
1959
- wireSearchInput(input.node, vars)
1960
- usageBySearchInput.set(input.node, usage)
1961
- })
2053
+ }
1962
2054
 
1963
2055
  // STEP 4.5: Wire the clear buttons, each to the input it shares a parent with.
1964
2056
  findAllSearchClearButtonsInJSX(blockStatement).forEach((button) => {
@@ -1997,14 +2089,36 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1997
2089
  }
1998
2090
  )
1999
2091
 
2000
- // STEP 6: Update getStaticProps if this is a page
2092
+ // STEP 5b: Wire the range texts that name their mapper.
2093
+ //
2094
+ // A `range` control carrying `data-tq-pagination-for` belongs to the mapper
2095
+ // whose editor node id it names, wherever it sits — a template's footer or
2096
+ // toolbar is laid out by the author, not inside a pagination container, so
2097
+ // the container-ownership rule above cannot reach it. One without the
2098
+ // attribute was wired by its container in STEP 5; one naming a mapper this
2099
+ // component does not render stays as drawn.
2100
+ rangeControls.forEach((control) => {
2101
+ const ownerNodeId = readJSXAttributeString(control, PAGINATION_FOR_ATTR)
2102
+ if (!ownerNodeId) {
2103
+ return
2104
+ }
2105
+ const owner = registry.usages.find((usage) => usage.nodeId === ownerNodeId)
2106
+ if (owner) {
2107
+ wireRangeControlFor(control, owner, getStateVarsForUsage(owner))
2108
+ }
2109
+ })
2110
+
2111
+ // STEP 6: Update getStaticProps if this is a page. Its rows are what the
2112
+ // page renders, so they are read as a visitor is served them — unless only
2113
+ // the store's trusted readers can open the page (`pageDataReadArguments`).
2001
2114
  if (isPage) {
2002
2115
  updateGetStaticProps(
2003
2116
  chunks,
2004
2117
  registry,
2005
2118
  dependencies,
2006
2119
  uidl.outputOptions?.folderPath,
2007
- localized
2120
+ localized,
2121
+ () => RouteUtils.pageDataReadArguments(uidl, options.auth)
2008
2122
  )
2009
2123
  }
2010
2124
 
@@ -2023,6 +2137,8 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
2023
2137
  * It has to be a sibling: the provider's own child is a render-prop function,
2024
2138
  * and anything placed inside it would be re-created on every fetch — the
2025
2139
  * observer would then be watching an element that is no longer in the document.
2140
+ * The sentinel is built for the element it lands in (`buildSentinelElement`):
2141
+ * a fragment is transparent, a component's children may go anywhere.
2026
2142
  *
2027
2143
  * Returns false when the provider has no element parent to splice into (a
2028
2144
  * provider returned directly from the component), leaving the list on its
@@ -2031,35 +2147,53 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
2031
2147
  function insertSentinelAfterProvider(
2032
2148
  blockStatement: types.BlockStatement,
2033
2149
  provider: any,
2034
- sentinel: types.JSXElement
2150
+ buildSentinel: (parentTag: string | null) => types.JSXElement
2035
2151
  ): boolean {
2036
2152
  let inserted = false
2037
2153
 
2038
- const traverse = (node: any): void => {
2154
+ const elementTagOf = (node: any, enclosing: string | null): string | null => {
2155
+ if (node?.type === 'JSXFragment') {
2156
+ return enclosing
2157
+ }
2158
+ if (node?.type !== 'JSXElement') {
2159
+ return enclosing
2160
+ }
2161
+ const name = node.openingElement?.name
2162
+ if (name?.type !== 'JSXIdentifier') {
2163
+ return null
2164
+ }
2165
+ if (name.name === 'Fragment') {
2166
+ return enclosing
2167
+ }
2168
+ return /^[a-z]/.test(name.name) ? name.name : null
2169
+ }
2170
+
2171
+ const traverse = (node: any, enclosing: string | null): void => {
2039
2172
  if (!node || inserted) {
2040
2173
  return
2041
2174
  }
2175
+ const tag = elementTagOf(node, enclosing)
2042
2176
  if (Array.isArray(node.children)) {
2043
2177
  const at = node.children.indexOf(provider)
2044
2178
  if (at !== -1) {
2045
- node.children.splice(at + 1, 0, sentinel)
2179
+ node.children.splice(at + 1, 0, buildSentinel(tag))
2046
2180
  inserted = true
2047
2181
  return
2048
2182
  }
2049
- node.children.forEach(traverse)
2183
+ node.children.forEach((child: any) => traverse(child, tag))
2050
2184
  }
2051
2185
  if (node.body) {
2052
2186
  if (Array.isArray(node.body)) {
2053
- node.body.forEach(traverse)
2187
+ node.body.forEach((child: any) => traverse(child, tag))
2054
2188
  } else {
2055
- traverse(node.body)
2189
+ traverse(node.body, tag)
2056
2190
  }
2057
2191
  }
2058
2192
  if (node.consequent) {
2059
- traverse(node.consequent)
2193
+ traverse(node.consequent, tag)
2060
2194
  }
2061
2195
  if (node.alternate) {
2062
- traverse(node.alternate)
2196
+ traverse(node.alternate, tag)
2063
2197
  }
2064
2198
  // `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
2065
2199
  // panel, a gated section: the provider sits on the RIGHT of a logical
@@ -2067,23 +2201,23 @@ function insertSentinelAfterProvider(
2067
2201
  // an argument. Left unvisited, the products list under the first tab of
2068
2202
  // a shop was never paginated or seeded, and rendered nothing.
2069
2203
  if (node.left) {
2070
- traverse(node.left)
2204
+ traverse(node.left, tag)
2071
2205
  }
2072
2206
  if (node.right) {
2073
- traverse(node.right)
2207
+ traverse(node.right, tag)
2074
2208
  }
2075
2209
  if (node.test) {
2076
- traverse(node.test)
2210
+ traverse(node.test, tag)
2077
2211
  }
2078
2212
  if (node.expression) {
2079
- traverse(node.expression)
2213
+ traverse(node.expression, tag)
2080
2214
  }
2081
2215
  if (node.argument) {
2082
- traverse(node.argument)
2216
+ traverse(node.argument, tag)
2083
2217
  }
2084
2218
  }
2085
2219
 
2086
- traverse(blockStatement)
2220
+ traverse(blockStatement, null)
2087
2221
  return inserted
2088
2222
  }
2089
2223
 
@@ -2199,68 +2333,6 @@ function findArrayMapperRenderPropInDataProvider(dataProvider: any): string | un
2199
2333
  return renderItemAttr.value.expression.params[0].name
2200
2334
  }
2201
2335
 
2202
- function findAllSearchInputsInJSX(
2203
- blockStatement: types.BlockStatement
2204
- ): Array<{ node: any; className: string }> {
2205
- const results: Array<{ node: any; className: string }> = []
2206
-
2207
- const traverse = (node: any): void => {
2208
- if (!node) {
2209
- return
2210
- }
2211
-
2212
- if (node.type === 'JSXElement' && node.openingElement?.name?.name === 'input') {
2213
- const classAttr = node.openingElement.attributes?.find(
2214
- (attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
2215
- )
2216
- const className = classAttr?.value?.value || classAttr?.value?.expression?.value || ''
2217
- if (className.includes('search-input')) {
2218
- results.push({ node, className })
2219
- }
2220
- }
2221
-
2222
- if (node.children && Array.isArray(node.children)) {
2223
- node.children.forEach((c: any) => traverse(c))
2224
- }
2225
- if (node.body) {
2226
- if (Array.isArray(node.body)) {
2227
- node.body.forEach((s: any) => traverse(s))
2228
- } else {
2229
- traverse(node.body)
2230
- }
2231
- }
2232
- if (node.consequent) {
2233
- traverse(node.consequent)
2234
- }
2235
- if (node.alternate) {
2236
- traverse(node.alternate)
2237
- }
2238
- // `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
2239
- // panel, a gated section: the provider sits on the RIGHT of a logical
2240
- // expression (or in a ternary's `test`), which is not a child, a body or
2241
- // an argument. Left unvisited, the products list under the first tab of
2242
- // a shop was never paginated or seeded, and rendered nothing.
2243
- if (node.left) {
2244
- traverse(node.left)
2245
- }
2246
- if (node.right) {
2247
- traverse(node.right)
2248
- }
2249
- if (node.test) {
2250
- traverse(node.test)
2251
- }
2252
- if (node.expression) {
2253
- traverse(node.expression)
2254
- }
2255
- if (node.argument) {
2256
- traverse(node.argument)
2257
- }
2258
- }
2259
-
2260
- traverse(blockStatement)
2261
- return results
2262
- }
2263
-
2264
2336
  /**
2265
2337
  * Every pagination widget in the JSX, paired with the mapper it belongs to.
2266
2338
  *
@@ -2343,10 +2415,12 @@ function findPaginationNodesWithOwners(
2343
2415
  *
2344
2416
  * An uncached mapper keeps the inline `params={useMemo(...)}` it has always had,
2345
2417
  * byte for byte. A cached one hoists that memo into a component const so the
2346
- * peek can depend on the same object identity instead of rebuilding it.
2418
+ * peek can depend on the same object identity instead of rebuilding it; so does
2419
+ * an appending list that starts from its page's rows (`hoistInto`), whose first
2420
+ * page is filed in the accumulator under those params.
2347
2421
  *
2348
- * Returns the identifier holding the peeked rows, or `undefined` when this
2349
- * mapper does not cache.
2422
+ * Returns the identifier holding the params when they were hoisted, and the one
2423
+ * holding the peeked rows when this mapper caches.
2350
2424
  */
2351
2425
  function pushParamsAttribute(
2352
2426
  // tslint:disable-next-line:no-any
@@ -2354,24 +2428,40 @@ function pushParamsAttribute(
2354
2428
  usage: DataSourceUsage,
2355
2429
  paramsProps: types.ObjectProperty[],
2356
2430
  memoDeps: types.Expression[],
2357
- cacheDeclarations: types.Statement[]
2358
- ): string | undefined {
2431
+ cacheDeclarations: types.Statement[],
2432
+ hoistInto?: types.Statement[]
2433
+ ): { paramsIdentifier?: string; cachedIdentifier?: string } {
2359
2434
  const paramsMemo = types.callExpression(types.identifier('useMemo'), [
2360
2435
  types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
2361
2436
  types.arrayExpression(memoDeps),
2362
2437
  ])
2363
2438
 
2364
- // A state-bound sort deliberately suppresses `initialData` (see the comment at
2365
- // the call site): the prefetch ran without sort params, so reusing anything on
2366
- // mount would mask the current sort. The cache peek is `initialData` by
2367
- // another name, so it has to be suppressed for exactly the same reason.
2439
+ // A state-bound sort gets no cache peek. The peek is `initialData` by another
2440
+ // name, and such a list takes the server's rows only while its sort is still
2441
+ // where the visitor starts (`buildStartingSortGuard`); the peek was never
2442
+ // extended to it.
2368
2443
  const cache = usage.cache?.client && !usage.dynamicSort ? usage.cache : undefined
2369
2444
 
2370
2445
  if (!cache) {
2446
+ if (!hoistInto) {
2447
+ dp.openingElement.attributes.push(
2448
+ types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(paramsMemo))
2449
+ )
2450
+ return {}
2451
+ }
2452
+ const paramsIdentifier = paramsVarFor(usage.index)
2453
+ hoistInto.push(
2454
+ types.variableDeclaration('const', [
2455
+ types.variableDeclarator(types.identifier(paramsIdentifier), paramsMemo),
2456
+ ])
2457
+ )
2371
2458
  dp.openingElement.attributes.push(
2372
- types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(paramsMemo))
2459
+ types.jsxAttribute(
2460
+ types.jsxIdentifier('params'),
2461
+ types.jsxExpressionContainer(types.identifier(paramsIdentifier))
2462
+ )
2373
2463
  )
2374
- return undefined
2464
+ return { paramsIdentifier }
2375
2465
  }
2376
2466
 
2377
2467
  const hoisted = buildCachedParamsDeclarations({ index: usage.index, paramsMemo, cache })
@@ -2384,7 +2474,97 @@ function pushParamsAttribute(
2384
2474
  )
2385
2475
  )
2386
2476
 
2387
- return hoisted.cachedIdentifier
2477
+ return { paramsIdentifier: hoisted.paramsIdentifier, cachedIdentifier: hoisted.cachedIdentifier }
2478
+ }
2479
+
2480
+ // Whether the rows `getStaticProps` fetched can be this list's first page at
2481
+ // all. A sort bound to state can only be served them when the build knows the
2482
+ // sort the visitor starts with (`startingSort`) and fetched by it; otherwise
2483
+ // the fetch has no sort, and its rows are not the list's first page.
2484
+ function prefetchFollowsSort(usage: DataSourceUsage): boolean {
2485
+ return !usage.dynamicSort || !!usage.startingSort
2486
+ }
2487
+
2488
+ // `sortBy === "name-asc"`: the visitor has not moved the sort from where it
2489
+ // starts, so the build-time rows are still its first page. `null` for a list
2490
+ // whose sort the visitor cannot pick, or one that reads no state.
2491
+ function buildStartingSortGuard(usage: DataSourceUsage): types.Expression | null {
2492
+ return usage.dynamicSort && usage.startingSort ? buildSortAtStartGuard(usage.startingSort) : null
2493
+ }
2494
+
2495
+ // `<condition> && <starting-sort guard>`, or the condition alone.
2496
+ function withStartingSortGuard(
2497
+ condition: types.Expression,
2498
+ usage: DataSourceUsage
2499
+ ): types.Expression {
2500
+ const sortGuard = buildStartingSortGuard(usage)
2501
+ return sortGuard ? types.logicalExpression('&&', condition, sortGuard) : condition
2502
+ }
2503
+
2504
+ // An appending list on a page starts from the rows its `getStaticProps`
2505
+ // fetched, when they can be its first page (`prefetchFollowsSort`).
2506
+ function startsFromServerRows(usage: DataSourceUsage): boolean {
2507
+ return usage.infiniteScroll && usage.paginated && prefetchFollowsSort(usage)
2508
+ }
2509
+
2510
+ /**
2511
+ * `initialData={ds_N_firstPage}` on an appending list, beside the frozen first
2512
+ * page and the effect that files it as page 1 of the accumulator (see
2513
+ * `infinite-scroll.ts`).
2514
+ *
2515
+ * The rows stand in for page 1 only while they answer the list's query: no
2516
+ * search (`queryGuard`), the sort where the visitor starts, no filter set (the
2517
+ * guards the paged lists use), and at least one row — an empty build-time
2518
+ * answer, which a failed fetch also gives, leaves the browser to fetch page 1
2519
+ * as before.
2520
+ */
2521
+ function pushInfiniteScrollFirstPage(
2522
+ // tslint:disable-next-line:no-any
2523
+ dp: any,
2524
+ usage: DataSourceUsage,
2525
+ vars: ReturnType<typeof getStateVarsForUsage>,
2526
+ paramsIdentifier: string,
2527
+ declarations: types.Statement[],
2528
+ queryGuard: types.Expression | null,
2529
+ instanceDeps: types.Expression[]
2530
+ ): void {
2531
+ const infiniteVars = getInfiniteScrollVars(usage.index)
2532
+ const serverPayload = (): types.Expression =>
2533
+ types.optionalMemberExpression(
2534
+ types.identifier('props'),
2535
+ types.identifier(vars.propsPrefix),
2536
+ false,
2537
+ true
2538
+ )
2539
+ const hasServerRows = types.binaryExpression(
2540
+ '>',
2541
+ types.optionalMemberExpression(
2542
+ buildServerRowsExpression(serverPayload(), usage.itemsPath),
2543
+ types.identifier('length'),
2544
+ false,
2545
+ true
2546
+ ),
2547
+ types.numericLiteral(0)
2548
+ )
2549
+ const startsFromServer = [
2550
+ queryGuard,
2551
+ buildStartingSortGuard(usage),
2552
+ buildNoUrlFilterGuard(usage),
2553
+ hasServerRows,
2554
+ ]
2555
+ .filter((guard): guard is types.Expression => guard !== null)
2556
+ .reduce((all, guard) => types.logicalExpression('&&', all, guard))
2557
+
2558
+ declarations.push(
2559
+ buildFirstPageDeclaration(infiniteVars, startsFromServer, serverPayload(), instanceDeps),
2560
+ buildFirstPageSeedEffect(infiniteVars, paramsIdentifier, usage.perPage, usage.itemsPath)
2561
+ )
2562
+ dp.openingElement.attributes.push(
2563
+ types.jsxAttribute(
2564
+ types.jsxIdentifier('initialData'),
2565
+ types.jsxExpressionContainer(types.identifier(infiniteVars.firstPageVar))
2566
+ )
2567
+ )
2388
2568
  }
2389
2569
 
2390
2570
  function updateDataProviderForPaginatedSearch(
@@ -2393,7 +2573,8 @@ function updateDataProviderForPaginatedSearch(
2393
2573
  vars: ReturnType<typeof getStateVarsForUsage>,
2394
2574
  fileName: string,
2395
2575
  cacheDeclarations: types.Statement[],
2396
- localized: boolean
2576
+ localized: boolean,
2577
+ firstPageDeclarations?: types.Statement[]
2397
2578
  ): void {
2398
2579
  const attrs = dp.openingElement.attributes
2399
2580
 
@@ -2452,36 +2633,67 @@ function updateDataProviderForPaginatedSearch(
2452
2633
  appendClientLocaleParam(paramsProps, memoDeps)
2453
2634
  }
2454
2635
 
2455
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2636
+ const seedInto = startsFromServerRows(usage) ? firstPageDeclarations : undefined
2637
+ const { paramsIdentifier, cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2638
+ dp,
2639
+ usage,
2640
+ paramsProps,
2641
+ memoDeps,
2642
+ cacheDeclarations,
2643
+ seedInto
2644
+ )
2456
2645
 
2457
- // Add initialData. Skip the server-prefetched data entirely when a dynamic
2458
- // state-bound sort is active — the prefetch ran without sort parameters, so
2459
- // reusing it would mask the current sort state AND cause DataProvider to skip
2460
- // the first client fetch (its internal guard only skips when initialData is
2461
- // defined on mount). Without that skip, toggling sort correctly triggers a
2462
- // refetch via the useMemo params dependency chain.
2646
+ // Add initialData: the server-prefetched page 1 while it is the page on show
2647
+ // — page 1, no search, no filter set, and a sort the visitor picks still where
2648
+ // they start (the prefetch ran by that sort). A list whose starting sort the
2649
+ // build cannot know never takes it: that prefetch ran without a sort, so
2650
+ // reusing it would mask the current sort AND make DataProvider skip its first
2651
+ // client fetch (its internal guard only skips when initialData is defined on
2652
+ // mount). Any other sort is fetched fresh, through the useMemo params.
2463
2653
  //
2464
- // An appending list skips it for a second reason: `initialData` makes
2465
- // DataProvider skip its first fetch, so page 1 would render from the prefetch
2466
- // and never enter the accumulator — appending page 2 would then REPLACE the
2467
- // visible rows with page 2 alone.
2468
- if (!usage.dynamicSort && !usage.infiniteScroll) {
2654
+ // An appending list takes its own: `initialData` makes DataProvider skip its
2655
+ // first fetch, so the prefetched page 1 has to be filed in the accumulator
2656
+ // too, or appending page 2 would REPLACE the visible rows with page 2 alone.
2657
+ // The provider remounts per query (its `key`), so the first page is frozen
2658
+ // per query.
2659
+ if (seedInto && paramsIdentifier) {
2660
+ const appliedQuery = (): types.MemberExpression =>
2661
+ types.memberExpression(
2662
+ types.identifier(vars.combinedStateVar),
2663
+ types.identifier('debouncedQuery')
2664
+ )
2665
+ pushInfiniteScrollFirstPage(
2666
+ dp,
2667
+ usage,
2668
+ vars,
2669
+ paramsIdentifier,
2670
+ seedInto,
2671
+ types.unaryExpression('!', appliedQuery(), true),
2672
+ [appliedQuery()]
2673
+ )
2674
+ } else if (prefetchFollowsSort(usage) && !usage.infiniteScroll) {
2469
2675
  const initialDataCondition = wrapInitialDataWithUrlFilterGuard(
2470
- types.logicalExpression(
2471
- '&&',
2472
- types.binaryExpression(
2473
- '===',
2474
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
2475
- types.numericLiteral(1)
2476
- ),
2477
- types.unaryExpression(
2478
- '!',
2479
- types.memberExpression(
2480
- types.identifier(vars.combinedStateVar),
2481
- types.identifier('debouncedQuery')
2676
+ withStartingSortGuard(
2677
+ types.logicalExpression(
2678
+ '&&',
2679
+ types.binaryExpression(
2680
+ '===',
2681
+ types.memberExpression(
2682
+ types.identifier(vars.combinedStateVar),
2683
+ types.identifier('page')
2684
+ ),
2685
+ types.numericLiteral(1)
2482
2686
  ),
2483
- true
2484
- )
2687
+ types.unaryExpression(
2688
+ '!',
2689
+ types.memberExpression(
2690
+ types.identifier(vars.combinedStateVar),
2691
+ types.identifier('debouncedQuery')
2692
+ ),
2693
+ true
2694
+ )
2695
+ ),
2696
+ usage
2485
2697
  ),
2486
2698
  usage
2487
2699
  )
@@ -2575,7 +2787,8 @@ function updateDataProviderForPaginationOnly(
2575
2787
  vars: ReturnType<typeof getStateVarsForUsage>,
2576
2788
  fileName: string,
2577
2789
  cacheDeclarations: types.Statement[],
2578
- localized: boolean
2790
+ localized: boolean,
2791
+ firstPageDeclarations?: types.Statement[]
2579
2792
  ): void {
2580
2793
  const attrs = dp.openingElement.attributes
2581
2794
 
@@ -2613,22 +2826,36 @@ function updateDataProviderForPaginationOnly(
2613
2826
  }
2614
2827
 
2615
2828
  // Add params
2616
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2829
+ const seedInto = startsFromServerRows(usage) ? firstPageDeclarations : undefined
2830
+ const { paramsIdentifier, cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2831
+ dp,
2832
+ usage,
2833
+ paramsProps,
2834
+ memoDeps,
2835
+ cacheDeclarations,
2836
+ seedInto
2837
+ )
2617
2838
 
2618
- // Add initialData. See paginated+search updater for why we skip prefetch
2619
- // reuse when a dynamic state-bound sort is active, and why an appending list
2620
- // skips it too.
2621
- if (!usage.dynamicSort && !usage.infiniteScroll) {
2839
+ // Add initialData. See paginated+search updater for when a sort the visitor
2840
+ // picks may reuse the prefetch, and how an appending list takes its own. This
2841
+ // provider never remounts (no `key`), so its first page is frozen for the
2842
+ // page's lifetime.
2843
+ if (seedInto && paramsIdentifier) {
2844
+ pushInfiniteScrollFirstPage(dp, usage, vars, paramsIdentifier, seedInto, null, [])
2845
+ } else if (prefetchFollowsSort(usage) && !usage.infiniteScroll) {
2622
2846
  dp.openingElement.attributes.push(
2623
2847
  types.jsxAttribute(
2624
2848
  types.jsxIdentifier('initialData'),
2625
2849
  types.jsxExpressionContainer(
2626
2850
  types.conditionalExpression(
2627
2851
  wrapInitialDataWithUrlFilterGuard(
2628
- types.binaryExpression(
2629
- '===',
2630
- types.identifier(vars.pageStateVar),
2631
- types.numericLiteral(1)
2852
+ withStartingSortGuard(
2853
+ types.binaryExpression(
2854
+ '===',
2855
+ types.identifier(vars.pageStateVar),
2856
+ types.numericLiteral(1)
2857
+ ),
2858
+ usage
2632
2859
  ),
2633
2860
  usage
2634
2861
  ),
@@ -2733,18 +2960,27 @@ function updateDataProviderForSearchOnly(
2733
2960
  appendClientLocaleParam(paramsProps, memoDeps)
2734
2961
  }
2735
2962
 
2736
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2963
+ const { cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2964
+ dp,
2965
+ usage,
2966
+ paramsProps,
2967
+ memoDeps,
2968
+ cacheDeclarations
2969
+ )
2737
2970
 
2738
- // Add initialData. See paginated+search updater for why we skip prefetch
2739
- // reuse when a dynamic state-bound sort is active.
2740
- if (!usage.dynamicSort) {
2971
+ // Add initialData. See paginated+search updater for when a sort the visitor
2972
+ // picks may reuse the prefetch.
2973
+ if (prefetchFollowsSort(usage)) {
2741
2974
  dp.openingElement.attributes.push(
2742
2975
  types.jsxAttribute(
2743
2976
  types.jsxIdentifier('initialData'),
2744
2977
  types.jsxExpressionContainer(
2745
2978
  types.conditionalExpression(
2746
2979
  wrapInitialDataWithUrlFilterGuard(
2747
- types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
2980
+ withStartingSortGuard(
2981
+ types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
2982
+ usage
2983
+ ),
2748
2984
  usage
2749
2985
  ),
2750
2986
  types.optionalMemberExpression(
@@ -2789,20 +3025,58 @@ function updateDataProviderForSearchOnly(
2789
3025
  )
2790
3026
  }
2791
3027
 
3028
+ /**
3029
+ * A plain list on a page is seeded with the rows its `getStaticProps` fetched
3030
+ * (`extractDataSourceIntoGetStaticProps`), and a seeded provider skips its first
3031
+ * fetch. With a sort the visitor picks, those rows are the list's only while the
3032
+ * sort is where a visitor starts — the build fetched them by it:
3033
+ *
3034
+ * initialData={sortBy === "name-asc" ? props.items_products : undefined}
3035
+ *
3036
+ * — and any other sort is fetched fresh, through the params it changes. When the
3037
+ * build cannot know where the sort starts (`prefetchFollowsSort`), it fetched
3038
+ * the rows unsorted, and the list is never seeded with them. A plain list has no
3039
+ * search, and is only seeded without a filter a visitor sets.
3040
+ */
3041
+ function seedPlainListWhileSortStarts(dp: any, usage: DataSourceUsage): void {
3042
+ if (!usage.dynamicSort) {
3043
+ return
3044
+ }
3045
+ const attrs = dp.openingElement.attributes
3046
+ const index = attrs.findIndex(
3047
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name?.name === 'initialData'
3048
+ )
3049
+ if (index === -1) {
3050
+ return
3051
+ }
3052
+ if (!prefetchFollowsSort(usage)) {
3053
+ attrs.splice(index, 1)
3054
+ return
3055
+ }
3056
+ const sortGuard = buildStartingSortGuard(usage)
3057
+ const seeded = (attrs[index] as types.JSXAttribute).value
3058
+ if (!sortGuard || seeded?.type !== 'JSXExpressionContainer') {
3059
+ return
3060
+ }
3061
+ attrs[index] = types.jsxAttribute(
3062
+ types.jsxIdentifier('initialData'),
3063
+ types.jsxExpressionContainer(
3064
+ types.conditionalExpression(
3065
+ sortGuard,
3066
+ seeded.expression as types.Expression,
3067
+ types.identifier('undefined')
3068
+ )
3069
+ )
3070
+ )
3071
+ }
3072
+
2792
3073
  function updateDataProviderForPlain(
2793
3074
  dp: any,
2794
3075
  fileName: string,
2795
3076
  usage: DataSourceUsage,
2796
3077
  localized: boolean
2797
3078
  ): void {
2798
- // A sort bound to state is the visitor's, which the server cannot know: rows
2799
- // pre-rendered in the table's order would stand in for it, since a seeded
2800
- // provider skips its first fetch — the paginated lists drop them the same way.
2801
- if (usage.dynamicSort) {
2802
- dp.openingElement.attributes = dp.openingElement.attributes.filter(
2803
- (attr: any) => !(attr.type === 'JSXAttribute' && attr.name?.name === 'initialData')
2804
- )
2805
- }
3079
+ seedPlainListWhileSortStarts(dp, usage)
2806
3080
 
2807
3081
  const attrs = dp.openingElement.attributes
2808
3082
 
@@ -3057,11 +3331,21 @@ function createFetchDataAttribute(
3057
3331
  [responseTail]
3058
3332
  )
3059
3333
 
3334
+ // An appending list records the request it now stands on before making it
3335
+ // (see `buildAccumulatingRequestMark`).
3060
3336
  const arrow = cache
3061
3337
  ? types.arrowFunctionExpression(
3062
3338
  [types.identifier('params')],
3063
3339
  buildCachedFetchBody(networkChain, cache)
3064
3340
  )
3341
+ : accumulate
3342
+ ? types.arrowFunctionExpression(
3343
+ [types.identifier('params')],
3344
+ types.blockStatement([
3345
+ buildAccumulatingRequestMark(getInfiniteScrollVars(accumulate.usage.index)),
3346
+ types.returnStatement(networkChain),
3347
+ ])
3348
+ )
3065
3349
  : types.arrowFunctionExpression([types.identifier('params')], networkChain)
3066
3350
 
3067
3351
  return types.jsxAttribute(
@@ -3072,39 +3356,6 @@ function createFetchDataAttribute(
3072
3356
  )
3073
3357
  }
3074
3358
 
3075
- function wireSearchInput(inputNode: any, vars: ReturnType<typeof getStateVarsForUsage>): void {
3076
- // Remove existing onChange and value
3077
- inputNode.openingElement.attributes = inputNode.openingElement.attributes.filter(
3078
- (attr: any) => !['onChange', 'value'].includes(attr.name?.name)
3079
- )
3080
-
3081
- // Add onChange
3082
- inputNode.openingElement.attributes.push(
3083
- types.jsxAttribute(
3084
- types.jsxIdentifier('onChange'),
3085
- types.jsxExpressionContainer(
3086
- types.arrowFunctionExpression(
3087
- [types.identifier('e')],
3088
- types.callExpression(types.identifier(vars.setSearchQueryVar), [
3089
- types.memberExpression(
3090
- types.memberExpression(types.identifier('e'), types.identifier('target')),
3091
- types.identifier('value')
3092
- ),
3093
- ])
3094
- )
3095
- )
3096
- )
3097
- )
3098
-
3099
- // Add value
3100
- inputNode.openingElement.attributes.push(
3101
- types.jsxAttribute(
3102
- types.jsxIdentifier('value'),
3103
- types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))
3104
- )
3105
- )
3106
- }
3107
-
3108
3359
  /**
3109
3360
  * The `fetchData` attribute for one paginated usage.
3110
3361
  *
@@ -3120,6 +3371,41 @@ function buildFetchDataForUsage(fileName: string, usage: DataSourceUsage): types
3120
3371
  return createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
3121
3372
  }
3122
3373
 
3374
+ /** `const [ds_N_total, setDs_N_total] = useState(null)` — declared only beside a range control. */
3375
+ function buildTotalDeclaration(vars: ReturnType<typeof getStateVarsForUsage>): types.Statement {
3376
+ return types.variableDeclaration('const', [
3377
+ types.variableDeclarator(
3378
+ types.arrayPattern([
3379
+ types.identifier(vars.totalStateVar),
3380
+ types.identifier(vars.setTotalStateVar),
3381
+ ]),
3382
+ types.callExpression(types.identifier('useState'), [types.nullLiteral()])
3383
+ ),
3384
+ ])
3385
+ }
3386
+
3387
+ /**
3388
+ * Fills one `range` text with the live row range of `usage` (see
3389
+ * `range-control.ts`). A mapper that does not page has no count to show, so
3390
+ * its range text is left as drawn.
3391
+ */
3392
+ function wireRangeControlFor(
3393
+ control: any,
3394
+ usage: DataSourceUsage,
3395
+ vars: ReturnType<typeof getStateVarsForUsage>
3396
+ ): void {
3397
+ if (!usage.paginated) {
3398
+ return
3399
+ }
3400
+ const { currentPageExpr } = getPageAccessors(usage, vars)
3401
+ wireRangeControl(control, {
3402
+ totalVar: vars.totalStateVar,
3403
+ currentPageExpr: currentPageExpr(),
3404
+ perPage: usage.perPage,
3405
+ infiniteScroll: usage.infiniteScroll,
3406
+ })
3407
+ }
3408
+
3123
3409
  /**
3124
3410
  * Wires the authored Load More button of an appending list.
3125
3411
  *
@@ -3185,6 +3471,12 @@ function wirePaginationWidget(
3185
3471
  vars: ReturnType<typeof getStateVarsForUsage>,
3186
3472
  hasLoadingState: boolean
3187
3473
  ): void {
3474
+ // A range text inside the container takes the container's owner, like the
3475
+ // buttons beside it; one naming its own mapper is wired in STEP 5b instead.
3476
+ findAllPaginationControls(paginationNode, 'range')
3477
+ .filter((control) => !readJSXAttributeString(control, PAGINATION_FOR_ATTR))
3478
+ .forEach((control) => wireRangeControlFor(control, usage, vars))
3479
+
3188
3480
  if (usage.infiniteScroll) {
3189
3481
  // Only the Load More button survives; page controls make no sense for a
3190
3482
  // list the visitor never leaves.
@@ -3599,12 +3891,24 @@ export default dataSourceModule.getCount
3599
3891
  }
3600
3892
  }
3601
3893
 
3894
+ // `fetchCount(<params>, <read arguments>)`: the params may be left out only
3895
+ // when nothing follows them.
3896
+ const countReadArguments = (
3897
+ countParams: types.ObjectProperty[],
3898
+ readArguments: types.Expression[]
3899
+ ): types.Expression[] =>
3900
+ countParams.length > 0 || readArguments.length > 0
3901
+ ? [types.objectExpression(countParams), ...readArguments]
3902
+ : []
3903
+
3602
3904
  function updateGetStaticProps(
3603
3905
  chunks: any[],
3604
3906
  registry: StateRegistry,
3605
3907
  dependencies: Record<string, any>,
3606
3908
  folderPath: string[] | undefined,
3607
- localized: boolean
3909
+ localized: boolean,
3910
+ // The arguments every read hands the module after its params (fresh nodes per call).
3911
+ readArguments: () => types.Expression[]
3608
3912
  ): void {
3609
3913
  const getStaticPropsChunk = chunks.find((c) => c.name === 'getStaticProps')
3610
3914
  if (!getStaticPropsChunk || getStaticPropsChunk.type !== ChunkType.AST) {
@@ -3736,6 +4040,16 @@ function updateGetStaticProps(
3736
4040
  )
3737
4041
  )
3738
4042
  )
4043
+ } else if (usage.dynamicSort && usage.startingSort) {
4044
+ // A sort the visitor picks: page 1 by the sort they start with, sent
4045
+ // as the very string the browser sends for it, so page 2 continues
4046
+ // this page exactly (see `buildStartingSortsValue`).
4047
+ fetchParams.push(
4048
+ types.objectProperty(
4049
+ types.identifier('sorts'),
4050
+ buildStartingSortsValue(usage.dynamicSort, usage.startingSort)
4051
+ )
4052
+ )
3739
4053
  }
3740
4054
 
3741
4055
  // Add filters if present
@@ -3773,7 +4087,7 @@ function updateGetStaticProps(
3773
4087
  types.identifier(fetcherImportName),
3774
4088
  types.identifier('fetchData')
3775
4089
  ),
3776
- [types.objectExpression(fetchParams)]
4090
+ [types.objectExpression(fetchParams), ...readArguments()]
3777
4091
  ),
3778
4092
  types.identifier('catch')
3779
4093
  ),
@@ -3962,7 +4276,7 @@ function updateGetStaticProps(
3962
4276
  types.identifier(fetcherImportName),
3963
4277
  types.identifier('fetchCount')
3964
4278
  ),
3965
- countParams.length > 0 ? [types.objectExpression(countParams)] : []
4279
+ countReadArguments(countParams, readArguments())
3966
4280
  ),
3967
4281
  types.identifier('catch')
3968
4282
  ),