@teleporthq/teleport-plugin-next-data-source 0.43.74 → 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 (231) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +95 -5
  2. package/__tests__/blog-adjacent-arranged.test.ts +176 -0
  3. package/__tests__/blog-post-contents.test.ts +212 -0
  4. package/__tests__/blog-post-page-enrichment.test.ts +28 -5
  5. package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
  6. package/__tests__/blog-post-reading-time.test.ts +55 -0
  7. package/__tests__/booking-item-transform.test.ts +120 -0
  8. package/__tests__/browser-row-policy.test.ts +13 -3
  9. package/__tests__/content-preset-wiring.test.ts +108 -0
  10. package/__tests__/fetchers.test.ts +1 -1
  11. package/__tests__/fixed-list-sort.test.ts +183 -0
  12. package/__tests__/infinite-scroll.test.ts +154 -5
  13. package/__tests__/list-sort-order.test.ts +61 -0
  14. package/__tests__/null-filter-where.test.ts +132 -0
  15. package/__tests__/pagination-filter-groups.test.ts +186 -0
  16. package/__tests__/pg-date-parser.test.ts +73 -0
  17. package/__tests__/plain-list-sort.test.ts +215 -0
  18. package/__tests__/product-price-sort.ts +58 -25
  19. package/__tests__/range-control.test.ts +159 -0
  20. package/__tests__/rest-api-key-header.test.ts +18 -0
  21. package/__tests__/search-input-marker.test.ts +86 -0
  22. package/__tests__/search-url-sync.test.ts +61 -16
  23. package/__tests__/shared-resource-prefetch.test.ts +65 -0
  24. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  25. package/dist/cjs/count-effect.d.ts +7 -1
  26. package/dist/cjs/count-effect.d.ts.map +1 -1
  27. package/dist/cjs/count-effect.js +15 -5
  28. package/dist/cjs/count-effect.js.map +1 -1
  29. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  30. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  31. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  32. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  33. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/postgresql.js +4 -1
  35. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  36. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/raw-query.js +2 -1
  38. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  39. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/rest-api.js +3 -1
  41. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  42. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/teleport.js +5 -2
  44. package/dist/cjs/fetchers/teleport.js.map +1 -1
  45. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/filter-utils.d.ts +19 -7
  50. package/dist/cjs/filter-utils.d.ts.map +1 -1
  51. package/dist/cjs/filter-utils.js +49 -13
  52. package/dist/cjs/filter-utils.js.map +1 -1
  53. package/dist/cjs/infinite-scroll.d.ts +25 -2
  54. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  55. package/dist/cjs/infinite-scroll.js +144 -33
  56. package/dist/cjs/infinite-scroll.js.map +1 -1
  57. package/dist/cjs/list-sort-order.d.ts +14 -0
  58. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  59. package/dist/cjs/list-sort-order.js +36 -0
  60. package/dist/cjs/list-sort-order.js.map +1 -0
  61. package/dist/cjs/pagination-controls.d.ts +22 -3
  62. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  63. package/dist/cjs/pagination-controls.js +29 -3
  64. package/dist/cjs/pagination-controls.js.map +1 -1
  65. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  66. package/dist/cjs/pagination-plugin.js +267 -212
  67. package/dist/cjs/pagination-plugin.js.map +1 -1
  68. package/dist/cjs/product-price-sort.d.ts +0 -9
  69. package/dist/cjs/product-price-sort.d.ts.map +1 -1
  70. package/dist/cjs/product-price-sort.js +1 -13
  71. package/dist/cjs/product-price-sort.js.map +1 -1
  72. package/dist/cjs/range-control.d.ts +45 -0
  73. package/dist/cjs/range-control.d.ts.map +1 -0
  74. package/dist/cjs/range-control.js +165 -0
  75. package/dist/cjs/range-control.js.map +1 -0
  76. package/dist/cjs/row-order.d.ts +20 -0
  77. package/dist/cjs/row-order.d.ts.map +1 -0
  78. package/dist/cjs/row-order.js +46 -0
  79. package/dist/cjs/row-order.js.map +1 -0
  80. package/dist/cjs/search-input.d.ts +42 -0
  81. package/dist/cjs/search-input.d.ts.map +1 -0
  82. package/dist/cjs/search-input.js +158 -0
  83. package/dist/cjs/search-input.js.map +1 -0
  84. package/dist/cjs/sort-utils.d.ts +10 -0
  85. package/dist/cjs/sort-utils.d.ts.map +1 -1
  86. package/dist/cjs/sort-utils.js +22 -1
  87. package/dist/cjs/sort-utils.js.map +1 -1
  88. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
  89. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
  90. package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
  91. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
  92. package/dist/cjs/transformations/blog-comments.d.ts +1 -1
  93. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
  94. package/dist/cjs/transformations/blog-comments.js +1 -1
  95. package/dist/cjs/transformations/blog-comments.js.map +1 -1
  96. package/dist/cjs/transformations/blog-post.d.ts +14 -0
  97. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  98. package/dist/cjs/transformations/blog-post.js +4 -1
  99. package/dist/cjs/transformations/blog-post.js.map +1 -1
  100. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  101. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  102. package/dist/cjs/transformations/booking-item.js +27 -0
  103. package/dist/cjs/transformations/booking-item.js.map +1 -0
  104. package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
  105. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
  106. package/dist/cjs/transformations/heading-anchors.js +34 -10
  107. package/dist/cjs/transformations/heading-anchors.js.map +1 -1
  108. package/dist/cjs/transformations/index.d.ts +27 -13
  109. package/dist/cjs/transformations/index.d.ts.map +1 -1
  110. package/dist/cjs/transformations/index.js +73 -16
  111. package/dist/cjs/transformations/index.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/utils.d.ts.map +1 -1
  114. package/dist/cjs/utils.js +52 -3
  115. package/dist/cjs/utils.js.map +1 -1
  116. package/dist/esm/count-effect.d.ts +7 -1
  117. package/dist/esm/count-effect.d.ts.map +1 -1
  118. package/dist/esm/count-effect.js +15 -5
  119. package/dist/esm/count-effect.js.map +1 -1
  120. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  121. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  122. package/dist/esm/fetchers/pg-client-import.js +12 -0
  123. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  124. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  125. package/dist/esm/fetchers/postgresql.js +5 -2
  126. package/dist/esm/fetchers/postgresql.js.map +1 -1
  127. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  128. package/dist/esm/fetchers/raw-query.js +2 -1
  129. package/dist/esm/fetchers/raw-query.js.map +1 -1
  130. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  131. package/dist/esm/fetchers/rest-api.js +3 -1
  132. package/dist/esm/fetchers/rest-api.js.map +1 -1
  133. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  134. package/dist/esm/fetchers/teleport.js +6 -3
  135. package/dist/esm/fetchers/teleport.js.map +1 -1
  136. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
  137. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  138. package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
  139. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  140. package/dist/esm/filter-utils.d.ts +19 -7
  141. package/dist/esm/filter-utils.d.ts.map +1 -1
  142. package/dist/esm/filter-utils.js +46 -12
  143. package/dist/esm/filter-utils.js.map +1 -1
  144. package/dist/esm/infinite-scroll.d.ts +25 -2
  145. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  146. package/dist/esm/infinite-scroll.js +142 -32
  147. package/dist/esm/infinite-scroll.js.map +1 -1
  148. package/dist/esm/list-sort-order.d.ts +14 -0
  149. package/dist/esm/list-sort-order.d.ts.map +1 -0
  150. package/dist/esm/list-sort-order.js +32 -0
  151. package/dist/esm/list-sort-order.js.map +1 -0
  152. package/dist/esm/pagination-controls.d.ts +22 -3
  153. package/dist/esm/pagination-controls.d.ts.map +1 -1
  154. package/dist/esm/pagination-controls.js +28 -2
  155. package/dist/esm/pagination-controls.js.map +1 -1
  156. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  157. package/dist/esm/pagination-plugin.js +271 -216
  158. package/dist/esm/pagination-plugin.js.map +1 -1
  159. package/dist/esm/product-price-sort.d.ts +0 -9
  160. package/dist/esm/product-price-sort.d.ts.map +1 -1
  161. package/dist/esm/product-price-sort.js +0 -11
  162. package/dist/esm/product-price-sort.js.map +1 -1
  163. package/dist/esm/range-control.d.ts +45 -0
  164. package/dist/esm/range-control.d.ts.map +1 -0
  165. package/dist/esm/range-control.js +137 -0
  166. package/dist/esm/range-control.js.map +1 -0
  167. package/dist/esm/row-order.d.ts +20 -0
  168. package/dist/esm/row-order.d.ts.map +1 -0
  169. package/dist/esm/row-order.js +41 -0
  170. package/dist/esm/row-order.js.map +1 -0
  171. package/dist/esm/search-input.d.ts +42 -0
  172. package/dist/esm/search-input.d.ts.map +1 -0
  173. package/dist/esm/search-input.js +129 -0
  174. package/dist/esm/search-input.js.map +1 -0
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -1
  177. package/dist/esm/sort-utils.js +20 -0
  178. package/dist/esm/sort-utils.js.map +1 -1
  179. package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
  180. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
  181. package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
  182. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
  183. package/dist/esm/transformations/blog-comments.d.ts +1 -1
  184. package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
  185. package/dist/esm/transformations/blog-comments.js +1 -1
  186. package/dist/esm/transformations/blog-comments.js.map +1 -1
  187. package/dist/esm/transformations/blog-post.d.ts +14 -0
  188. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  189. package/dist/esm/transformations/blog-post.js +4 -1
  190. package/dist/esm/transformations/blog-post.js.map +1 -1
  191. package/dist/esm/transformations/booking-item.d.ts +24 -0
  192. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  193. package/dist/esm/transformations/booking-item.js +23 -0
  194. package/dist/esm/transformations/booking-item.js.map +1 -0
  195. package/dist/esm/transformations/heading-anchors.d.ts +32 -8
  196. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
  197. package/dist/esm/transformations/heading-anchors.js +33 -9
  198. package/dist/esm/transformations/heading-anchors.js.map +1 -1
  199. package/dist/esm/transformations/index.d.ts +27 -13
  200. package/dist/esm/transformations/index.d.ts.map +1 -1
  201. package/dist/esm/transformations/index.js +72 -16
  202. package/dist/esm/transformations/index.js.map +1 -1
  203. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  204. package/dist/esm/utils.d.ts.map +1 -1
  205. package/dist/esm/utils.js +52 -3
  206. package/dist/esm/utils.js.map +1 -1
  207. package/package.json +5 -5
  208. package/src/count-effect.ts +31 -8
  209. package/src/fetchers/pg-client-import.ts +16 -0
  210. package/src/fetchers/postgresql.ts +25 -23
  211. package/src/fetchers/raw-query.ts +2 -1
  212. package/src/fetchers/rest-api.ts +3 -1
  213. package/src/fetchers/teleport.ts +22 -23
  214. package/src/fetchers/utils/browser-row-policy.ts +10 -20
  215. package/src/filter-utils.ts +102 -28
  216. package/src/infinite-scroll.ts +240 -79
  217. package/src/list-sort-order.ts +34 -0
  218. package/src/pagination-controls.ts +26 -3
  219. package/src/pagination-plugin.ts +370 -306
  220. package/src/product-price-sort.ts +0 -11
  221. package/src/range-control.ts +207 -0
  222. package/src/row-order.ts +55 -0
  223. package/src/search-input.ts +190 -0
  224. package/src/sort-utils.ts +25 -0
  225. package/src/transformations/blog-adjacent-posts.ts +89 -3
  226. package/src/transformations/blog-comments.ts +5 -5
  227. package/src/transformations/blog-post.ts +64 -13
  228. package/src/transformations/booking-item.ts +71 -0
  229. package/src/transformations/heading-anchors.ts +79 -16
  230. package/src/transformations/index.ts +104 -14
  231. package/src/utils.ts +74 -3
@@ -48,21 +48,23 @@ import * as types from '@babel/types';
48
48
  import { parseExpression } from '@babel/parser';
49
49
  import { ASTStatementOrder, GenericUtils, StringUtils } from '@teleporthq/teleport-shared';
50
50
  import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common';
51
- import { generateSafeFileName } from './utils';
51
+ import { collectFilterLeafConditions, generateSafeFileName, pruneFilterTree } from './utils';
52
52
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers';
53
53
  import { buildProductTransformOptions } from './transformations';
54
- import { appendSortsParam, extractDynamicSort } from './sort-utils';
55
- import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils';
54
+ import { appendSortsParam, extractDynamicSort, readFixedSort } from './sort-utils';
55
+ import { appendFiltersParam, buildStaticFiltersStringifyCall, pushStateIdsAsDeps, pushPropIdsAsDeps, } from './filter-utils';
56
56
  import { appendItemsPathParam, extractItemsPath } from './items-path';
57
57
  import { resolveRepeaterPerPage } from './repeater-page-size';
58
58
  import { applyLoadingStateToDataProvider, buildLoadingStateDeclarations, getLoadingStateVars, } from './loading-state';
59
59
  import { mergeServerCacheOptions, resolveCacheFromRepeaterContent } from './cache/config';
60
60
  import { buildCacheHydrationEffect, buildCacheStoreThen, buildCachedFetchBody, buildCachedParamsDeclarations, registerCacheClientImports, } from './cache/ast';
61
- import { convertControlToButton, dropInactivePaginationControls, findPaginationControl, setJSXExpressionAttribute, } from './pagination-controls';
61
+ import { convertControlToButton, dropInactivePaginationControls, findAllPaginationControls, findPaginationControl, PAGINATION_FOR_ATTR, readJSXAttributeString, setJSXExpressionAttribute, } from './pagination-controls';
62
+ import { findRangeControls, wireRangeControl } from './range-control';
63
+ import { findAllSearchInputsInJSX, pairSearchInputs, wireSearchInput } from './search-input';
62
64
  import { buildCountFetchEffect, buildCountSeqDeclaration } from './count-effect';
63
65
  import { findAllSearchClearButtonsInJSX, findSiblingSearchInput, wireSearchClearButton, } from './search-clear';
64
66
  import { buildPageTokensDeclaration, wireNumberedPagination, } from './numbered-pagination';
65
- import { buildAccumulatingResponseHandler, buildInfiniteScrollDeclarations, buildSentinelElement, buildSentinelObserverEffect, getInfiniteScrollVars, } from './infinite-scroll';
67
+ import { buildAccumulatingRequestMark, buildAccumulatingResponseHandler, buildInfiniteScrollDeclarations, buildSentinelElement, buildSentinelObserverEffect, getInfiniteScrollVars, } from './infinite-scroll';
66
68
  import { buildFilterSortPageResetEffect, buildPageResetRefDeclaration, hasNoPageResetDeps, } from './page-reset';
67
69
  import { buildPageUrlRefDeclaration, buildPageUrlSyncEffect } from './page-url-sync';
68
70
  import { isLocalizedProject } from './content-localization';
@@ -103,79 +105,37 @@ function searchDefaultValueInitAST(value) {
103
105
  // independent AST node — Babel does not support shared references.
104
106
  return types.cloneNode(value.ast, /* deep */ true);
105
107
  }
106
- // Builds the `useState(...)` initializer for the search query. When the
107
- // cms-list-repeater is bound to a URL param (`searchUrlParamKey`), the query is
108
- // seeded from `window.location.search` on the client so a deep link such as
109
- // `/products-list?searchKeyword=yoga` arrives pre-filtered on the very first
110
- // paint; the server render (where `window` is undefined) and any missing param
111
- // fall back to the `searchDefaultValue` seed (empty string by default). This
112
- // mirrors the `selectedCategory` / `sortBy` initializer emitted by
113
- // `createStateHookAST` for state-level `urlSearchParamBinding`, keeping the
114
- // three products-list controls consistent.
108
+ // Builds the `useState(...)` initializer for the search query: the
109
+ // `searchDefaultValue` seed (empty string by default), never the address. A
110
+ // statically generated page is rendered on the server without the visitor's
111
+ // `?searchKeyword=`, and the browser's first render must match it — a query
112
+ // seeded from `window.location.search` rendered a filled input and skipped the
113
+ // prefetched rows there, and React warned of a hydration mismatch. A URL-bound
114
+ // search adopts the address after mount instead (`pushSearchUrlSyncEffects`),
115
+ // as the page number does (`page-url-sync.ts`).
115
116
  //
116
117
  // Returns a fresh AST on every call (no shared node references) so the same
117
118
  // initializer can seed both the immediate `ds_N_searchQuery` state and the
118
119
  // debounced `ds_N_state.debouncedQuery` value.
119
120
  function buildSearchQueryInitAST(usage) {
120
- var fallback = searchDefaultValueInitAST(usage.searchDefaultValue);
121
- if (!usage.searchUrlParamKey) {
122
- return fallback;
123
- }
124
- // (typeof window !== 'undefined'
125
- // ? new URLSearchParams(window.location.search).get('<key>')
126
- // : null) ?? <fallback>
127
- return types.logicalExpression('??', types.conditionalExpression(types.binaryExpression('!==', types.unaryExpression('typeof', types.identifier('window')), types.stringLiteral('undefined')), types.callExpression(types.memberExpression(types.newExpression(types.identifier('URLSearchParams'), [
128
- types.memberExpression(types.memberExpression(types.identifier('window'), types.identifier('location')), types.identifier('search')),
129
- ]), types.identifier('get')), [types.stringLiteral(usage.searchUrlParamKey)]), types.nullLiteral()), fallback);
121
+ return searchDefaultValueInitAST(usage.searchDefaultValue);
130
122
  }
131
- // The UIDL's `filters.content` array can wrap one or more conditions inside
132
- // a `{ type: 'group', operator: 'and' | 'or', children: [...] }` entry — the
133
- // GUI builds this shape when the inspector adds a logical group. The data
134
- // source API endpoint (`processFilters` in the generated data-source module)
135
- // only knows how to consume a FLAT array of `{ source, destination, operand }`
136
- // conditions, so we walk groups recursively here and collect their leaf
137
- // conditions in order. Anything that isn't a group AND isn't a condition is
138
- // dropped — defensive against partially-built filter entries that would
139
- // otherwise emit `{ source: '', destination: '', operand: '' }` rows the
140
- // API ignores anyway.
141
- //
142
- // `or`-grouped conditions are flattened just like `and`-grouped ones — the
143
- // downstream SQL builder always joins flat filters with `AND`, so emitting
144
- // the conditions side-by-side approximates AND semantics. A future API
145
- // upgrade that respects group operators would key off the original tree
146
- // directly; until then, AND-flattening is the closest correct behaviour and
147
- // matches what the inspector preview shows for the common single-group case
148
- // the GUI emits today.
149
- function flattenFilterGroups(filters) {
123
+ // The UIDL's `filters.content` array is the tree the inspector authored:
124
+ // `{ type: 'group', operator: 'and' | 'or', children: [...] }` entries around
125
+ // `{ source, destination, operand }` conditions, nested as deep as the user
126
+ // grouped them. The tree is kept as it is — the generated fetchers read it with
127
+ // `normalizeFilterTree`, so an `or` group stays an `or`. Only junk is dropped:
128
+ // an entry that is neither a group nor a condition (a half-built inspector
129
+ // row) would otherwise reach the API as `{ source: '', destination: '' }`.
130
+ // Entries without an explicit `type` are the legacy flat form and are
131
+ // conditions.
132
+ function readFilterTree(filters) {
150
133
  if (!Array.isArray(filters)) {
151
134
  return [];
152
135
  }
153
- var out = [];
154
- var walk = function (entry) {
155
- if (!entry || typeof entry !== 'object') {
156
- return;
157
- }
158
- if (entry.type === 'group' && Array.isArray(entry.children)) {
159
- for (var _i = 0, _a = entry.children; _i < _a.length; _i++) {
160
- var child = _a[_i];
161
- walk(child);
162
- }
163
- return;
164
- }
165
- // Backwards-compatible: entries without an explicit `type` (legacy flat
166
- // form) and entries explicitly tagged `condition` are both treated as
167
- // condition leaves.
168
- if (entry.type === undefined || entry.type === 'condition') {
169
- out.push(entry);
170
- }
171
- };
172
- for (var _i = 0, filters_1 = filters; _i < filters_1.length; _i++) {
173
- var entry = filters_1[_i];
174
- walk(entry);
175
- }
176
- return out;
136
+ return pruneFilterTree(filters, function (entry) { return entry.type === undefined || entry.type === 'condition'; });
177
137
  }
178
- // Walks a flat filter list to extract every `urlSearchParams` reference key
138
+ // Walks the filter tree to extract every `urlSearchParams` reference key
179
139
  // (e.g. `'categoryFilter'`). Used to (a) inject `const router = useRouter()`
180
140
  // once at the top of the component, (b) wire `router.query.<key>` into the
181
141
  // `useMemo` dependency array so the client-side fetch reruns whenever the
@@ -184,8 +144,8 @@ function flattenFilterGroups(filters) {
184
144
  function collectFilterUrlSearchParamKeys(filters) {
185
145
  var seen = new Set();
186
146
  var out = [];
187
- for (var _i = 0, filters_2 = filters; _i < filters_2.length; _i++) {
188
- var f = filters_2[_i];
147
+ for (var _i = 0, _a = collectFilterLeafConditions(filters); _i < _a.length; _i++) {
148
+ var f = _a[_i];
189
149
  var dest = f === null || f === void 0 ? void 0 : f.destination;
190
150
  if (!ASTUtils.isUIDLDynamicReference(dest)) {
191
151
  continue;
@@ -202,6 +162,12 @@ function collectFilterUrlSearchParamKeys(filters) {
202
162
  }
203
163
  return out;
204
164
  }
165
+ // The filter tree with every dynamic destination pruned — what `getStaticProps`
166
+ // can resolve at build time. Groups keep their operator; a group left empty by
167
+ // the pruning disappears with it (`pruneFilterTree`).
168
+ function staticFiltersOf(usage) {
169
+ return pruneFilterTree(usage.filters || [], function (f) { return !ASTUtils.isUIDLDynamicReference(f.destination); });
170
+ }
205
171
  // Returns true when any filter destination is a `urlSearchParams` dynamic
206
172
  // reference — used by the plugin to know it needs to import `useRouter` and
207
173
  // emit `const router = useRouter()` at the top of the component. Cheaper to
@@ -359,22 +325,28 @@ function buildStateRegistry(uidlNode) {
359
325
  if ((_k = (_j = parentDataSource.resourceParams) === null || _j === void 0 ? void 0 : _j.sorts) === null || _k === void 0 ? void 0 : _k.content) {
360
326
  sorts = parentDataSource.resourceParams.sorts.content;
361
327
  }
362
- // If legacy sorts aren't set, fall back to the new dynamic single-column
363
- // sort fields on the cms-list-repeater (used by admin-panel listing pages).
328
+ // If legacy sorts aren't set, fall back to the single-column sort fields
329
+ // on the cms-list-repeater. A FIXED one (a column chosen in the editor,
330
+ // e.g. a blog listing's newest first) is a static sort: read as dynamic,
331
+ // it made the list drop the server-side first page (`initialData` below
332
+ // is skipped for a dynamic sort) — the pre-rendered HTML carried no
333
+ // posts, and the browser fetched page 1 a second time. Only a sort
334
+ // bound to state (the admin panel's sortable columns) is dynamic.
364
335
  var dynamicSort = void 0;
365
336
  if ((!sorts || sorts.length === 0) && content.sort) {
366
- dynamicSort = extractDynamicSort(content.sort, content.sortDirection);
337
+ var fixedSort = readFixedSort(content.sort, content.sortDirection);
338
+ if (fixedSort) {
339
+ sorts = [fixedSort];
340
+ }
341
+ else {
342
+ dynamicSort = extractDynamicSort(content.sort, content.sortDirection);
343
+ }
367
344
  }
368
- // Extract filters from parent's resource params. The inspector wraps
369
- // every condition in a `{ type: 'group' }` envelope (single-group or
370
- // nested), so flatten to leaf conditions before the downstream emit
371
- // sites consume `.source` / `.destination` / `.operand` directly —
372
- // they expect a flat condition array. See `flattenFilterGroups`'s
373
- // header comment for why AND-flattening is the correct fallback for
374
- // the API endpoint's flat-condition contract.
345
+ // Extract filters from parent's resource params, keeping the group tree
346
+ // the inspector authored (see `readFilterTree`).
375
347
  var filters = [];
376
348
  if ((_m = (_l = parentDataSource.resourceParams) === null || _l === void 0 ? void 0 : _l.filters) === null || _m === void 0 ? void 0 : _m.content) {
377
- filters = flattenFilterGroups(parentDataSource.resourceParams.filters.content);
349
+ filters = readFilterTree(parentDataSource.resourceParams.filters.content);
378
350
  }
379
351
  // Split dynamic destination keys by reference type. `state`/`prop`
380
352
  // refs resolve to bare identifiers (so they're useMemo deps as-is);
@@ -382,8 +354,8 @@ function buildStateRegistry(uidlNode) {
382
354
  // `useRouter()` declaration injected separately.
383
355
  var filterStateIds = [];
384
356
  var filterPropIds = [];
385
- for (var _i = 0, filters_3 = filters; _i < filters_3.length; _i++) {
386
- var f = filters_3[_i];
357
+ for (var _i = 0, _u = collectFilterLeafConditions(filters); _i < _u.length; _i++) {
358
+ var f = _u[_i];
387
359
  if (!ASTUtils.isUIDLDynamicReference(f.destination)) {
388
360
  continue;
389
361
  }
@@ -437,6 +409,7 @@ function buildStateRegistry(uidlNode) {
437
409
  index: index++,
438
410
  dataSourceIdentifier: parentDataSource.identifier,
439
411
  arrayMapperRenderProp: arrayMapperRenderProp,
412
+ nodeId: typeof content.nodeId === 'string' ? content.nodeId : undefined,
440
413
  resourceDefinition: {
441
414
  dataSourceId: parentDataSource.resourceDef.dataSourceId || '',
442
415
  tableName: parentDataSource.resourceDef.tableName || '',
@@ -489,8 +462,8 @@ function buildStateRegistry(uidlNode) {
489
462
  }
490
463
  // Recurse into children
491
464
  if (((_r = node.content) === null || _r === void 0 ? void 0 : _r.children) && Array.isArray(node.content.children)) {
492
- for (var _u = 0, _v = node.content.children; _u < _v.length; _u++) {
493
- var child = _v[_u];
465
+ for (var _v = 0, _w = node.content.children; _v < _w.length; _v++) {
466
+ var child = _w[_v];
494
467
  traverse(child, parentDataSource);
495
468
  }
496
469
  }
@@ -515,8 +488,8 @@ function buildStateRegistry(uidlNode) {
515
488
  }
516
489
  }
517
490
  if (Array.isArray(node.children)) {
518
- for (var _w = 0, _x = node.children; _w < _x.length; _w++) {
519
- var child = _x[_w];
491
+ for (var _x = 0, _y = node.children; _x < _y.length; _x++) {
492
+ var child = _y[_x];
520
493
  traverse(child, parentDataSource);
521
494
  }
522
495
  }
@@ -532,6 +505,8 @@ function getStateVarsForUsage(usage) {
532
505
  setPageStateVar: "setDs_".concat(idx, "_page"),
533
506
  maxPagesStateVar: "ds_".concat(idx, "_maxPages"),
534
507
  setMaxPagesStateVar: "setDs_".concat(idx, "_maxPages"),
508
+ totalStateVar: "ds_".concat(idx, "_total"),
509
+ setTotalStateVar: "setDs_".concat(idx, "_total"),
535
510
  searchQueryVar: "ds_".concat(idx, "_searchQuery"),
536
511
  setSearchQueryVar: "setDs_".concat(idx, "_searchQuery"),
537
512
  debouncedSearchQueryVar: "ds_".concat(idx, "_debouncedQuery"),
@@ -596,11 +571,64 @@ function pushSearchUrlSyncEffects(effectStatements, usage, vars) {
596
571
  ? types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery'))
597
572
  : types.identifier(vars.debouncedSearchQueryVar);
598
573
  };
574
+ // The query starts at its default (see `buildSearchQueryInitAST`), so the
575
+ // pair must agree on who goes first: the read-back adopts the address, THEN
576
+ // the write-back may write — before that, its first run would delete the very
577
+ // `?<key>=` the read-back is about to adopt.
578
+ var adoptedRefVar = "ds_".concat(usage.index, "_searchUrlAdopted");
579
+ var adopterVar = "ds_".concat(usage.index, "_adoptUrlSearch");
580
+ effectStatements.push(types.variableDeclaration('const', [
581
+ types.variableDeclarator(types.identifier(adoptedRefVar), types.callExpression(types.identifier('useRef'), [types.booleanLiteral(false)])),
582
+ ]), buildUrlSearchAdopter(adopterVar, usage, vars));
599
583
  // write-back: debounced query → `?<searchUrlParamKey>=`
600
- effectStatements.push(URLSearchParamSync.buildUrlWriteBackEffect(usage.searchUrlParamKey, buildDebouncedValueExpr(), buildDebouncedValueExpr()));
601
- // read-back: `?<searchUrlParamKey>=` → input (`setDs_N_searchQuery`). The
602
- // debounce effect propagates the change into the debounced value + fetch.
603
- effectStatements.push(URLSearchParamSync.buildUrlReadBackEffect(usage.searchUrlParamKey, vars.setSearchQueryVar));
584
+ effectStatements.push(URLSearchParamSync.buildUrlWriteBackEffect(usage.searchUrlParamKey, buildDebouncedValueExpr(), buildDebouncedValueExpr(), undefined, adoptedRefVar));
585
+ // read-back: `?<searchUrlParamKey>=` → the input AND the applied query at once
586
+ // (`ds_N_adoptUrlSearch`), so an address is not a typed change: it skips the
587
+ // debounce and keeps a page the address carries — `?searchKeyword=x&page=3`
588
+ // lands on page 3.
589
+ effectStatements.push(URLSearchParamSync.buildUrlReadBackEffect(usage.searchUrlParamKey, adopterVar, undefined, adoptedRefVar));
590
+ }
591
+ // `const ds_N_adoptUrlSearch = (update) => { … }`: the setter the search's URL
592
+ // read-back calls with its `(prev) => next` updater. It moves the input and the
593
+ // applied (debounced) query together. A list whose page is in the address
594
+ // keeps its page — the page sync sets it from the same address. Any other
595
+ // list starts the new query on page 1, as a typed change does in the debounce
596
+ // effect: an appending list has usually advanced past page 1 by the time the
597
+ // router is ready, and the query's first answer would otherwise be its page 2
598
+ // — empty for a query only a few rows match.
599
+ function buildUrlSearchAdopter(adopterVar, usage, vars) {
600
+ var update = types.identifier('update');
601
+ var pageReset = usageHasPageUrlSync(usage)
602
+ ? []
603
+ : [types.objectProperty(types.identifier('page'), types.numericLiteral(1))];
604
+ var applyToDebounced = usage.category === 'paginated+search'
605
+ ? // setDs_N_state((state) => {
606
+ // const next = update(state.debouncedQuery)
607
+ // return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next }
608
+ // })
609
+ types.callExpression(types.identifier(vars.setCombinedStateVar), [
610
+ types.arrowFunctionExpression([types.identifier('state')], types.blockStatement([
611
+ types.variableDeclaration('const', [
612
+ types.variableDeclarator(types.identifier('next'), types.callExpression(types.identifier('update'), [
613
+ types.memberExpression(types.identifier('state'), types.identifier('debouncedQuery')),
614
+ ])),
615
+ ]),
616
+ types.returnStatement(types.conditionalExpression(types.binaryExpression('===', types.identifier('next'), types.memberExpression(types.identifier('state'), types.identifier('debouncedQuery'))), types.identifier('state'), types.objectExpression(__spreadArray(__spreadArray([
617
+ types.spreadElement(types.identifier('state'))
618
+ ], pageReset, true), [
619
+ types.objectProperty(types.identifier('debouncedQuery'), types.identifier('next')),
620
+ ], false)))),
621
+ ])),
622
+ ])
623
+ : types.callExpression(types.identifier(vars.setDebouncedSearchQueryVar), [update]);
624
+ return types.variableDeclaration('const', [
625
+ types.variableDeclarator(types.identifier(adopterVar), types.arrowFunctionExpression([types.identifier('update')], types.blockStatement([
626
+ types.expressionStatement(types.callExpression(types.identifier(vars.setSearchQueryVar), [
627
+ types.identifier('update'),
628
+ ])),
629
+ types.expressionStatement(applyToDebounced),
630
+ ]))),
631
+ ]);
604
632
  }
605
633
  // How one usage reads and writes its current page.
606
634
  //
@@ -752,7 +780,7 @@ function pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usag
752
780
  // ==================== MAIN PLUGIN ====================
753
781
  export var createNextArrayMapperPaginationPlugin = function () {
754
782
  var paginationPlugin = function (structure) { return __awaiter(void 0, void 0, void 0, function () {
755
- var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, localized, stateDeclarations, effectStatements, needsUseRouter, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, usagesWithLoadingState, usageByProviderNode, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages, usageBySearchInput;
783
+ var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, rangeControls, hasRangeControl, registry, getStaticPropsChunk, isPage, localized, stateDeclarations, effectStatements, needsUseRouter, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, usagesWithLoadingState, usageByProviderNode, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, usageBySearchInput, _i, searchInputs_1, input, usage;
756
784
  var _a, _b;
757
785
  return __generator(this, function (_c) {
758
786
  uidl = structure.uidl, chunks = structure.chunks, dependencies = structure.dependencies, options = structure.options;
@@ -776,6 +804,8 @@ export var createNextArrayMapperPaginationPlugin = function () {
776
804
  return [2 /*return*/, structure];
777
805
  }
778
806
  blockStatement = arrowFunction.body;
807
+ rangeControls = findRangeControls(blockStatement);
808
+ hasRangeControl = rangeControls.length > 0;
779
809
  registry = buildStateRegistry(uidl.node);
780
810
  if (registry.usages.length === 0) {
781
811
  return [2 /*return*/, structure];
@@ -862,13 +892,12 @@ export var createNextArrayMapperPaginationPlugin = function () {
862
892
  ]),
863
893
  ])),
864
894
  ]));
865
- // Immediate search query state — seeded with `searchDefaultValue` (or
866
- // the `searchUrlParamKey` URL value when bound) so the input is
867
- // pre-filled on mount. The debounced value above is seeded identically
868
- // so a deep-linked `?searchKeyword=` fetches filtered results on the
869
- // first paint: the paginated+search `initialData` is gated on
870
- // `!ds_N_state.debouncedQuery`, so a non-empty seed forces the
871
- // DataProvider to fetch instead of reusing the unfiltered SSG prefetch.
895
+ // Immediate search query state — seeded with `searchDefaultValue`, like
896
+ // the debounced value above. A deep-linked `?searchKeyword=` is adopted
897
+ // after mount by the URL read-back, which sets both at once; the
898
+ // paginated+search `initialData` is gated on `!ds_N_state.debouncedQuery`,
899
+ // so the adopted query makes the DataProvider fetch filtered rows
900
+ // instead of reusing the unfiltered SSG prefetch the first render shows.
872
901
  stateDeclarations.push(types.variableDeclaration('const', [
873
902
  types.variableDeclarator(types.arrayPattern([
874
903
  types.identifier(vars.searchQueryVar),
@@ -945,11 +974,15 @@ export var createNextArrayMapperPaginationPlugin = function () {
945
974
  pushPropIdsAsDeps(countEffectDeps, countEffectSeen, usage.filterPropIds);
946
975
  // Same goes for URL-driven filters (already documented above).
947
976
  pushUrlSearchParamMemoDeps(countEffectDeps, usage);
977
+ if (hasRangeControl) {
978
+ stateDeclarations.push(buildTotalDeclaration(vars));
979
+ }
948
980
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar));
949
981
  effectStatements.push(buildCountFetchEffect({
950
982
  fileName: fileName,
951
983
  perPage: usage.perPage,
952
984
  setMaxPagesVar: vars.setMaxPagesStateVar,
985
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
953
986
  urlParams: urlParams,
954
987
  deps: countEffectDeps,
955
988
  countSeqRefVar: vars.countSeqRefVar,
@@ -991,7 +1024,9 @@ export var createNextArrayMapperPaginationPlugin = function () {
991
1024
  // "Next" button, but not to label a numbered strip or to know when an
992
1025
  // infinite list has reached its end, so those two modes fetch a live
993
1026
  // count as well.
994
- var needsLiveCount = !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll;
1027
+ // A range text needs the live count too: the build-time count only
1028
+ // ever became a page count, never the total it shows.
1029
+ var needsLiveCount = !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll || hasRangeControl;
995
1030
  if (needsLiveCount) {
996
1031
  var fileName = generateSafeFileName(usage.resourceDefinition.dataSourceType, usage.resourceDefinition.tableName, usage.resourceDefinition.dataSourceId);
997
1032
  // ⛔ This list has no search input, so no `query` param — but it may
@@ -1013,11 +1048,15 @@ export var createNextArrayMapperPaginationPlugin = function () {
1013
1048
  pushStateIdsAsDeps(countDeps, countDepsSeen, usage.filterStateIds);
1014
1049
  pushPropIdsAsDeps(countDeps, countDepsSeen, usage.filterPropIds);
1015
1050
  pushUrlSearchParamMemoDeps(countDeps, usage);
1051
+ if (hasRangeControl) {
1052
+ stateDeclarations.push(buildTotalDeclaration(vars));
1053
+ }
1016
1054
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar));
1017
1055
  effectStatements.push(buildCountFetchEffect({
1018
1056
  fileName: fileName,
1019
1057
  perPage: usage.perPage,
1020
1058
  setMaxPagesVar: vars.setMaxPagesStateVar,
1059
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
1021
1060
  urlParams: countUrlParams,
1022
1061
  deps: countDeps,
1023
1062
  countSeqRefVar: vars.countSeqRefVar,
@@ -1148,7 +1187,9 @@ export var createNextArrayMapperPaginationPlugin = function () {
1148
1187
  // An auto-loading list needs something at the end of the rows for its
1149
1188
  // IntersectionObserver to watch.
1150
1189
  if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
1151
- insertSentinelAfterProvider(blockStatement, dp, buildSentinelElement(getInfiniteScrollVars(usage.index)));
1190
+ insertSentinelAfterProvider(blockStatement, dp, function (parentTag) {
1191
+ return buildSentinelElement(getInfiniteScrollVars(usage.index), parentTag);
1192
+ });
1152
1193
  }
1153
1194
  // Create API route for all categories (including 'plain' for components)
1154
1195
  ensureAPIRouteExists(options.extractedResources, usage, options.dataSources, buildProductTransformOptions(options));
@@ -1190,17 +1231,14 @@ export var createNextArrayMapperPaginationPlugin = function () {
1190
1231
  stabilizeDataProviderWithoutRepeater(dp);
1191
1232
  });
1192
1233
  searchInputs = findAllSearchInputsInJSX(blockStatement);
1193
- searchEnabledUsages = registry.usages.filter(function (u) { return u.searchEnabled; });
1194
- usageBySearchInput = new Map();
1195
- searchInputs.forEach(function (input, idx) {
1196
- if (idx >= searchEnabledUsages.length) {
1197
- return;
1234
+ usageBySearchInput = pairSearchInputs(searchInputs, registry.usages);
1235
+ for (_i = 0, searchInputs_1 = searchInputs; _i < searchInputs_1.length; _i++) {
1236
+ input = searchInputs_1[_i];
1237
+ usage = usageBySearchInput.get(input.node);
1238
+ if (usage) {
1239
+ wireSearchInput(input, getStateVarsForUsage(usage));
1198
1240
  }
1199
- var usage = searchEnabledUsages[idx];
1200
- var vars = getStateVarsForUsage(usage);
1201
- wireSearchInput(input.node, vars);
1202
- usageBySearchInput.set(input.node, usage);
1203
- });
1241
+ }
1204
1242
  // STEP 4.5: Wire the clear buttons, each to the input it shares a parent with.
1205
1243
  findAllSearchClearButtonsInJSX(blockStatement).forEach(function (button) {
1206
1244
  var input = findSiblingSearchInput(button, searchInputs.map(function (entry) { return entry.node; }));
@@ -1227,6 +1265,24 @@ export var createNextArrayMapperPaginationPlugin = function () {
1227
1265
  }
1228
1266
  wirePaginationWidget(node, usage, getStateVarsForUsage(usage), usagesWithLoadingState.has(usage.index));
1229
1267
  });
1268
+ // STEP 5b: Wire the range texts that name their mapper.
1269
+ //
1270
+ // A `range` control carrying `data-tq-pagination-for` belongs to the mapper
1271
+ // whose editor node id it names, wherever it sits — a template's footer or
1272
+ // toolbar is laid out by the author, not inside a pagination container, so
1273
+ // the container-ownership rule above cannot reach it. One without the
1274
+ // attribute was wired by its container in STEP 5; one naming a mapper this
1275
+ // component does not render stays as drawn.
1276
+ rangeControls.forEach(function (control) {
1277
+ var ownerNodeId = readJSXAttributeString(control, PAGINATION_FOR_ATTR);
1278
+ if (!ownerNodeId) {
1279
+ return;
1280
+ }
1281
+ var owner = registry.usages.find(function (usage) { return usage.nodeId === ownerNodeId; });
1282
+ if (owner) {
1283
+ wireRangeControlFor(control, owner, getStateVarsForUsage(owner));
1284
+ }
1285
+ });
1230
1286
  // STEP 6: Update getStaticProps if this is a page
1231
1287
  if (isPage) {
1232
1288
  updateGetStaticProps(chunks, registry, dependencies, (_b = uidl.outputOptions) === null || _b === void 0 ? void 0 : _b.folderPath, localized);
@@ -1244,39 +1300,59 @@ export var createNextArrayMapperPaginationPlugin = function () {
1244
1300
  * It has to be a sibling: the provider's own child is a render-prop function,
1245
1301
  * and anything placed inside it would be re-created on every fetch — the
1246
1302
  * observer would then be watching an element that is no longer in the document.
1303
+ * The sentinel is built for the element it lands in (`buildSentinelElement`):
1304
+ * a fragment is transparent, a component's children may go anywhere.
1247
1305
  *
1248
1306
  * Returns false when the provider has no element parent to splice into (a
1249
1307
  * provider returned directly from the component), leaving the list on its
1250
1308
  * count-driven `hasMore` rather than injecting a stray node.
1251
1309
  */
1252
- function insertSentinelAfterProvider(blockStatement, provider, sentinel) {
1310
+ function insertSentinelAfterProvider(blockStatement, provider, buildSentinel) {
1253
1311
  var inserted = false;
1254
- var traverse = function (node) {
1312
+ var elementTagOf = function (node, enclosing) {
1313
+ var _a;
1314
+ if ((node === null || node === void 0 ? void 0 : node.type) === 'JSXFragment') {
1315
+ return enclosing;
1316
+ }
1317
+ if ((node === null || node === void 0 ? void 0 : node.type) !== 'JSXElement') {
1318
+ return enclosing;
1319
+ }
1320
+ var name = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.name;
1321
+ if ((name === null || name === void 0 ? void 0 : name.type) !== 'JSXIdentifier') {
1322
+ return null;
1323
+ }
1324
+ if (name.name === 'Fragment') {
1325
+ return enclosing;
1326
+ }
1327
+ return /^[a-z]/.test(name.name) ? name.name : null;
1328
+ };
1329
+ var traverse = function (node, enclosing) {
1255
1330
  if (!node || inserted) {
1256
1331
  return;
1257
1332
  }
1333
+ var tag = elementTagOf(node, enclosing);
1258
1334
  if (Array.isArray(node.children)) {
1259
1335
  var at = node.children.indexOf(provider);
1260
1336
  if (at !== -1) {
1261
- node.children.splice(at + 1, 0, sentinel);
1337
+ node.children.splice(at + 1, 0, buildSentinel(tag));
1262
1338
  inserted = true;
1263
1339
  return;
1264
1340
  }
1265
- node.children.forEach(traverse);
1341
+ node.children.forEach(function (child) { return traverse(child, tag); });
1266
1342
  }
1267
1343
  if (node.body) {
1268
1344
  if (Array.isArray(node.body)) {
1269
- node.body.forEach(traverse);
1345
+ node.body.forEach(function (child) { return traverse(child, tag); });
1270
1346
  }
1271
1347
  else {
1272
- traverse(node.body);
1348
+ traverse(node.body, tag);
1273
1349
  }
1274
1350
  }
1275
1351
  if (node.consequent) {
1276
- traverse(node.consequent);
1352
+ traverse(node.consequent, tag);
1277
1353
  }
1278
1354
  if (node.alternate) {
1279
- traverse(node.alternate);
1355
+ traverse(node.alternate, tag);
1280
1356
  }
1281
1357
  // `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
1282
1358
  // panel, a gated section: the provider sits on the RIGHT of a logical
@@ -1284,22 +1360,22 @@ function insertSentinelAfterProvider(blockStatement, provider, sentinel) {
1284
1360
  // an argument. Left unvisited, the products list under the first tab of
1285
1361
  // a shop was never paginated or seeded, and rendered nothing.
1286
1362
  if (node.left) {
1287
- traverse(node.left);
1363
+ traverse(node.left, tag);
1288
1364
  }
1289
1365
  if (node.right) {
1290
- traverse(node.right);
1366
+ traverse(node.right, tag);
1291
1367
  }
1292
1368
  if (node.test) {
1293
- traverse(node.test);
1369
+ traverse(node.test, tag);
1294
1370
  }
1295
1371
  if (node.expression) {
1296
- traverse(node.expression);
1372
+ traverse(node.expression, tag);
1297
1373
  }
1298
1374
  if (node.argument) {
1299
- traverse(node.argument);
1375
+ traverse(node.argument, tag);
1300
1376
  }
1301
1377
  };
1302
- traverse(blockStatement);
1378
+ traverse(blockStatement, null);
1303
1379
  return inserted;
1304
1380
  }
1305
1381
  function findAllDataProvidersInJSX(blockStatement) {
@@ -1394,61 +1470,6 @@ function findArrayMapperRenderPropInDataProvider(dataProvider) {
1394
1470
  }
1395
1471
  return renderItemAttr.value.expression.params[0].name;
1396
1472
  }
1397
- function findAllSearchInputsInJSX(blockStatement) {
1398
- var results = [];
1399
- var traverse = function (node) {
1400
- var _a, _b, _c, _d, _e, _f;
1401
- if (!node) {
1402
- return;
1403
- }
1404
- if (node.type === 'JSXElement' && ((_b = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.name) === 'input') {
1405
- var classAttr = (_c = node.openingElement.attributes) === null || _c === void 0 ? void 0 : _c.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'className'; });
1406
- var className = ((_d = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _d === void 0 ? void 0 : _d.value) || ((_f = (_e = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _e === void 0 ? void 0 : _e.expression) === null || _f === void 0 ? void 0 : _f.value) || '';
1407
- if (className.includes('search-input')) {
1408
- results.push({ node: node, className: className });
1409
- }
1410
- }
1411
- if (node.children && Array.isArray(node.children)) {
1412
- node.children.forEach(function (c) { return traverse(c); });
1413
- }
1414
- if (node.body) {
1415
- if (Array.isArray(node.body)) {
1416
- node.body.forEach(function (s) { return traverse(s); });
1417
- }
1418
- else {
1419
- traverse(node.body);
1420
- }
1421
- }
1422
- if (node.consequent) {
1423
- traverse(node.consequent);
1424
- }
1425
- if (node.alternate) {
1426
- traverse(node.alternate);
1427
- }
1428
- // `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
1429
- // panel, a gated section: the provider sits on the RIGHT of a logical
1430
- // expression (or in a ternary's `test`), which is not a child, a body or
1431
- // an argument. Left unvisited, the products list under the first tab of
1432
- // a shop was never paginated or seeded, and rendered nothing.
1433
- if (node.left) {
1434
- traverse(node.left);
1435
- }
1436
- if (node.right) {
1437
- traverse(node.right);
1438
- }
1439
- if (node.test) {
1440
- traverse(node.test);
1441
- }
1442
- if (node.expression) {
1443
- traverse(node.expression);
1444
- }
1445
- if (node.argument) {
1446
- traverse(node.argument);
1447
- }
1448
- };
1449
- traverse(blockStatement);
1450
- return results;
1451
- }
1452
1473
  /**
1453
1474
  * Every pagination widget in the JSX, paired with the mapper it belongs to.
1454
1475
  *
@@ -1734,7 +1755,13 @@ function updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclara
1734
1755
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.booleanLiteral(true))));
1735
1756
  }
1736
1757
  function updateDataProviderForPlain(dp, fileName, usage, localized) {
1737
- var _a, _b, _c, _d;
1758
+ var _a, _b, _c, _d, _e, _f;
1759
+ // A sort bound to state is the visitor's, which the server cannot know: rows
1760
+ // pre-rendered in the table's order would stand in for it, since a seeded
1761
+ // provider skips its first fetch — the paginated lists drop them the same way.
1762
+ if (usage.dynamicSort) {
1763
+ dp.openingElement.attributes = dp.openingElement.attributes.filter(function (attr) { var _a; return !(attr.type === 'JSXAttribute' && ((_a = attr.name) === null || _a === void 0 ? void 0 : _a.name) === 'initialData'); });
1764
+ }
1738
1765
  var attrs = dp.openingElement.attributes;
1739
1766
  var clientCache = ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined;
1740
1767
  // Check if fetchData already exists
@@ -1757,10 +1784,12 @@ function updateDataProviderForPlain(dp, fileName, usage, localized) {
1757
1784
  }
1758
1785
  // Find the params attribute. A plain list may have none at all — but the
1759
1786
  // fetch it was just given sends `params` regardless, and in a localized
1760
- // project those have to carry the locale, so the attribute is created.
1787
+ // project those have to carry the locale, and a sorted list its sort, so the
1788
+ // attribute is created.
1761
1789
  var paramsAttrIndex = attrs.findIndex(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'params'; });
1790
+ var isSorted = usage.sorts.length > 0 || !!usage.dynamicSort;
1762
1791
  if (paramsAttrIndex === -1) {
1763
- if (!localized) {
1792
+ if (!localized && !isSorted) {
1764
1793
  return;
1765
1794
  }
1766
1795
  attrs.push(types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(types.objectExpression([]))));
@@ -1781,10 +1810,17 @@ function updateDataProviderForPlain(dp, fileName, usage, localized) {
1781
1810
  if (!paramsExpression) {
1782
1811
  return;
1783
1812
  }
1784
- // Build useMemo dependencies including filter state IDs
1813
+ // The repeater's sort, which the paginated and searchable lists add to the
1814
+ // params they rebuild: kept as they come, a plain list's params never
1815
+ // carried it, and the rows arrived in the table's own order.
1816
+ if (paramsExpression.type === 'ObjectExpression' && !hasObjectKey(paramsExpression, 'sorts')) {
1817
+ appendSortsParam(paramsExpression.properties, usage.sorts, usage.dynamicSort);
1818
+ }
1819
+ // Build useMemo dependencies including filter state IDs and a dynamic sort's state
1785
1820
  var memoDeps = [];
1786
1821
  var memoSeen = new Set();
1787
1822
  pushStateIdsAsDeps(memoDeps, memoSeen, usage.filterStateIds);
1823
+ pushStateIdsAsDeps(memoDeps, memoSeen, (_f = (_e = usage.dynamicSort) === null || _e === void 0 ? void 0 : _e.depStateIds) !== null && _f !== void 0 ? _f : []);
1788
1824
  pushPropIdsAsDeps(memoDeps, memoSeen, usage.filterPropIds);
1789
1825
  pushUrlSearchParamMemoDeps(memoDeps, usage);
1790
1826
  if (localized && paramsExpression.type === 'ObjectExpression') {
@@ -1798,6 +1834,13 @@ function updateDataProviderForPlain(dp, fileName, usage, localized) {
1798
1834
  // Replace the params attribute
1799
1835
  attrs[paramsAttrIndex] = types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(memoizedParams));
1800
1836
  }
1837
+ var hasObjectKey = function (object, key) {
1838
+ return object.properties.some(function (property) {
1839
+ return property.type === 'ObjectProperty' &&
1840
+ ((property.key.type === 'Identifier' && property.key.name === key) ||
1841
+ (property.key.type === 'StringLiteral' && property.key.value === key));
1842
+ });
1843
+ };
1801
1844
  function stabilizeDataProviderWithoutRepeater(dp) {
1802
1845
  var _a, _b, _c;
1803
1846
  var attrs = dp.openingElement.attributes;
@@ -1871,21 +1914,18 @@ accumulate) {
1871
1914
  ? buildCacheStoreThen(cache)
1872
1915
  : uncachedTail;
1873
1916
  var networkChain = types.callExpression(types.memberExpression(parsedJson, types.identifier('then')), [responseTail]);
1917
+ // An appending list records the request it now stands on before making it
1918
+ // (see `buildAccumulatingRequestMark`).
1874
1919
  var arrow = cache
1875
1920
  ? types.arrowFunctionExpression([types.identifier('params')], buildCachedFetchBody(networkChain, cache))
1876
- : types.arrowFunctionExpression([types.identifier('params')], networkChain);
1921
+ : accumulate
1922
+ ? types.arrowFunctionExpression([types.identifier('params')], types.blockStatement([
1923
+ buildAccumulatingRequestMark(getInfiniteScrollVars(accumulate.usage.index)),
1924
+ types.returnStatement(networkChain),
1925
+ ]))
1926
+ : types.arrowFunctionExpression([types.identifier('params')], networkChain);
1877
1927
  return types.jsxAttribute(types.jsxIdentifier('fetchData'), types.jsxExpressionContainer(types.callExpression(types.identifier('useCallback'), [arrow, types.arrayExpression([])])));
1878
1928
  }
1879
- function wireSearchInput(inputNode, vars) {
1880
- // Remove existing onChange and value
1881
- inputNode.openingElement.attributes = inputNode.openingElement.attributes.filter(function (attr) { var _a; return !['onChange', 'value'].includes((_a = attr.name) === null || _a === void 0 ? void 0 : _a.name); });
1882
- // Add onChange
1883
- inputNode.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('onChange'), types.jsxExpressionContainer(types.arrowFunctionExpression([types.identifier('e')], types.callExpression(types.identifier(vars.setSearchQueryVar), [
1884
- types.memberExpression(types.memberExpression(types.identifier('e'), types.identifier('target')), types.identifier('value')),
1885
- ])))));
1886
- // Add value
1887
- inputNode.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('value'), types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))));
1888
- }
1889
1929
  /**
1890
1930
  * The `fetchData` attribute for one paginated usage.
1891
1931
  *
@@ -1901,6 +1941,32 @@ function buildFetchDataForUsage(fileName, usage) {
1901
1941
  }
1902
1942
  return createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined);
1903
1943
  }
1944
+ /** `const [ds_N_total, setDs_N_total] = useState(null)` — declared only beside a range control. */
1945
+ function buildTotalDeclaration(vars) {
1946
+ return types.variableDeclaration('const', [
1947
+ types.variableDeclarator(types.arrayPattern([
1948
+ types.identifier(vars.totalStateVar),
1949
+ types.identifier(vars.setTotalStateVar),
1950
+ ]), types.callExpression(types.identifier('useState'), [types.nullLiteral()])),
1951
+ ]);
1952
+ }
1953
+ /**
1954
+ * Fills one `range` text with the live row range of `usage` (see
1955
+ * `range-control.ts`). A mapper that does not page has no count to show, so
1956
+ * its range text is left as drawn.
1957
+ */
1958
+ function wireRangeControlFor(control, usage, vars) {
1959
+ if (!usage.paginated) {
1960
+ return;
1961
+ }
1962
+ var currentPageExpr = getPageAccessors(usage, vars).currentPageExpr;
1963
+ wireRangeControl(control, {
1964
+ totalVar: vars.totalStateVar,
1965
+ currentPageExpr: currentPageExpr(),
1966
+ perPage: usage.perPage,
1967
+ infiniteScroll: usage.infiniteScroll,
1968
+ });
1969
+ }
1904
1970
  /**
1905
1971
  * Wires the authored Load More button of an appending list.
1906
1972
  *
@@ -1940,6 +2006,11 @@ function wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState) {
1940
2006
  * buttons beside its Load More.
1941
2007
  */
1942
2008
  function wirePaginationWidget(paginationNode, usage, vars, hasLoadingState) {
2009
+ // A range text inside the container takes the container's owner, like the
2010
+ // buttons beside it; one naming its own mapper is wired in STEP 5b instead.
2011
+ findAllPaginationControls(paginationNode, 'range')
2012
+ .filter(function (control) { return !readJSXAttributeString(control, PAGINATION_FOR_ATTR); })
2013
+ .forEach(function (control) { return wireRangeControlFor(control, usage, vars); });
1943
2014
  if (usage.infiniteScroll) {
1944
2015
  // Only the Load More button survives; page controls make no sense for a
1945
2016
  // list the visitor never leaves.
@@ -2227,17 +2298,9 @@ function updateGetStaticProps(chunks, registry, dependencies, folderPath, locali
2227
2298
  }
2228
2299
  // Add filters if present
2229
2300
  // Filter out dynamic destinations for getStaticProps (they can't be resolved at build time)
2230
- var staticFilters = (usage.filters || []).filter(function (f) { return !ASTUtils.isUIDLDynamicReference(f.destination); });
2301
+ var staticFilters = staticFiltersOf(usage);
2231
2302
  if (staticFilters.length > 0) {
2232
- fetchParams.push(types.objectProperty(types.identifier('filters'), types.callExpression(types.memberExpression(types.identifier('JSON'), types.identifier('stringify')), [
2233
- types.arrayExpression(staticFilters.map(function (filter) {
2234
- return types.objectExpression([
2235
- types.objectProperty(types.identifier('source'), types.stringLiteral(filter.source || '')),
2236
- types.objectProperty(types.identifier('destination'), types.stringLiteral(typeof filter.destination === 'string' ? filter.destination : '')),
2237
- types.objectProperty(types.identifier('operand'), types.stringLiteral(filter.operand || '')),
2238
- ]);
2239
- })),
2240
- ])));
2303
+ fetchParams.push(types.objectProperty(types.identifier('filters'), buildStaticFiltersStringifyCall(staticFilters)));
2241
2304
  }
2242
2305
  appendItemsPathParam(fetchParams, usage.itemsPath, 'itemsPath');
2243
2306
  // The prefetch is per locale — getStaticProps runs once per language.
@@ -2277,7 +2340,7 @@ function updateGetStaticProps(chunks, registry, dependencies, folderPath, locali
2277
2340
  var maxPagesPropName_1 = "".concat(vars.propsPrefix, "_maxPages");
2278
2341
  // Use filter-specific count variable if usage has static filters
2279
2342
  // (dynamic filters can't be resolved at build time)
2280
- var staticFiltersForCount = (usage.filters || []).filter(function (f) { return !ASTUtils.isUIDLDynamicReference(f.destination); });
2343
+ var staticFiltersForCount = staticFiltersOf(usage);
2281
2344
  // A count scoped to an inner array is a different number from the
2282
2345
  // whole-payload one, so it gets its own variable just like a filtered one.
2283
2346
  var hasScopedCount = staticFiltersForCount.length > 0 || !!usage.itemsPath;
@@ -2328,7 +2391,7 @@ function updateGetStaticProps(chunks, registry, dependencies, folderPath, locali
2328
2391
  usagesByFilters.forEach(function (usages, filtersKey) {
2329
2392
  var firstUsage = usages[0];
2330
2393
  // Filter out dynamic destinations for getStaticProps (they can't be resolved at build time)
2331
- var staticFiltersForCount = (firstUsage.filters || []).filter(function (f) { return !ASTUtils.isUIDLDynamicReference(f.destination); });
2394
+ var staticFiltersForCount = staticFiltersOf(firstUsage);
2332
2395
  var hasStaticFilters = staticFiltersForCount.length > 0;
2333
2396
  // Create unique count variable name based on filters (and inner-array
2334
2397
  // scope — must match the naming at the maxPages site above)
@@ -2348,15 +2411,7 @@ function updateGetStaticProps(chunks, registry, dependencies, folderPath, locali
2348
2411
  // Build count params if static filters exist
2349
2412
  var countParams = [];
2350
2413
  if (hasStaticFilters) {
2351
- countParams.push(types.objectProperty(types.identifier('filters'), types.callExpression(types.memberExpression(types.identifier('JSON'), types.identifier('stringify')), [
2352
- types.arrayExpression(staticFiltersForCount.map(function (f) {
2353
- return types.objectExpression([
2354
- types.objectProperty(types.identifier('source'), types.stringLiteral(f.source || '')),
2355
- types.objectProperty(types.identifier('destination'), types.stringLiteral(typeof f.destination === 'string' ? f.destination : '')),
2356
- types.objectProperty(types.identifier('operand'), types.stringLiteral(f.operand || '')),
2357
- ]);
2358
- })),
2359
- ])));
2414
+ countParams.push(types.objectProperty(types.identifier('filters'), buildStaticFiltersStringifyCall(staticFiltersForCount)));
2360
2415
  }
2361
2416
  appendItemsPathParam(countParams, firstUsage.itemsPath, 'collectionPath');
2362
2417
  fetchesArray.elements.push(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.identifier(fetcherImportName), types.identifier('fetchCount')), countParams.length > 0 ? [types.objectExpression(countParams)] : []), types.identifier('catch')), [