@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +87 -4
  2. package/__tests__/booking-item-transform.test.ts +120 -0
  3. package/__tests__/browser-row-policy.test.ts +8 -2
  4. package/__tests__/fetchers.test.ts +1 -1
  5. package/__tests__/fixed-list-sort.test.ts +1 -1
  6. package/__tests__/infinite-scroll.test.ts +154 -5
  7. package/__tests__/list-sort-order.test.ts +61 -0
  8. package/__tests__/null-filter-where.test.ts +132 -0
  9. package/__tests__/pg-date-parser.test.ts +73 -0
  10. package/__tests__/plain-list-sort.test.ts +1 -1
  11. package/__tests__/product-price-sort.ts +11 -6
  12. package/__tests__/range-control.test.ts +159 -0
  13. package/__tests__/search-input-marker.test.ts +86 -0
  14. package/__tests__/search-url-sync.test.ts +34 -8
  15. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  16. package/dist/cjs/count-effect.d.ts +7 -1
  17. package/dist/cjs/count-effect.d.ts.map +1 -1
  18. package/dist/cjs/count-effect.js +15 -5
  19. package/dist/cjs/count-effect.js.map +1 -1
  20. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  21. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  22. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  23. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  24. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  25. package/dist/cjs/fetchers/postgresql.js +3 -1
  26. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  27. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  28. package/dist/cjs/fetchers/raw-query.js +2 -1
  29. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  30. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  31. package/dist/cjs/fetchers/teleport.js +4 -2
  32. package/dist/cjs/fetchers/teleport.js.map +1 -1
  33. package/dist/cjs/infinite-scroll.d.ts +25 -2
  34. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  35. package/dist/cjs/infinite-scroll.js +144 -33
  36. package/dist/cjs/infinite-scroll.js.map +1 -1
  37. package/dist/cjs/list-sort-order.d.ts +14 -0
  38. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  39. package/dist/cjs/list-sort-order.js +36 -0
  40. package/dist/cjs/list-sort-order.js.map +1 -0
  41. package/dist/cjs/pagination-controls.d.ts +22 -3
  42. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  43. package/dist/cjs/pagination-controls.js +29 -3
  44. package/dist/cjs/pagination-controls.js.map +1 -1
  45. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  46. package/dist/cjs/pagination-plugin.js +137 -101
  47. package/dist/cjs/pagination-plugin.js.map +1 -1
  48. package/dist/cjs/range-control.d.ts +45 -0
  49. package/dist/cjs/range-control.d.ts.map +1 -0
  50. package/dist/cjs/range-control.js +165 -0
  51. package/dist/cjs/range-control.js.map +1 -0
  52. package/dist/cjs/row-order.d.ts +3 -2
  53. package/dist/cjs/row-order.d.ts.map +1 -1
  54. package/dist/cjs/row-order.js +5 -4
  55. package/dist/cjs/row-order.js.map +1 -1
  56. package/dist/cjs/search-input.d.ts +42 -0
  57. package/dist/cjs/search-input.d.ts.map +1 -0
  58. package/dist/cjs/search-input.js +158 -0
  59. package/dist/cjs/search-input.js.map +1 -0
  60. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  61. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  62. package/dist/cjs/transformations/booking-item.js +27 -0
  63. package/dist/cjs/transformations/booking-item.js.map +1 -0
  64. package/dist/cjs/transformations/index.d.ts +4 -2
  65. package/dist/cjs/transformations/index.d.ts.map +1 -1
  66. package/dist/cjs/transformations/index.js +14 -5
  67. package/dist/cjs/transformations/index.js.map +1 -1
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/esm/count-effect.d.ts +7 -1
  70. package/dist/esm/count-effect.d.ts.map +1 -1
  71. package/dist/esm/count-effect.js +15 -5
  72. package/dist/esm/count-effect.js.map +1 -1
  73. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  74. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  75. package/dist/esm/fetchers/pg-client-import.js +12 -0
  76. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  77. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  78. package/dist/esm/fetchers/postgresql.js +3 -1
  79. package/dist/esm/fetchers/postgresql.js.map +1 -1
  80. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  81. package/dist/esm/fetchers/raw-query.js +2 -1
  82. package/dist/esm/fetchers/raw-query.js.map +1 -1
  83. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  84. package/dist/esm/fetchers/teleport.js +4 -2
  85. package/dist/esm/fetchers/teleport.js.map +1 -1
  86. package/dist/esm/infinite-scroll.d.ts +25 -2
  87. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  88. package/dist/esm/infinite-scroll.js +142 -32
  89. package/dist/esm/infinite-scroll.js.map +1 -1
  90. package/dist/esm/list-sort-order.d.ts +14 -0
  91. package/dist/esm/list-sort-order.d.ts.map +1 -0
  92. package/dist/esm/list-sort-order.js +32 -0
  93. package/dist/esm/list-sort-order.js.map +1 -0
  94. package/dist/esm/pagination-controls.d.ts +22 -3
  95. package/dist/esm/pagination-controls.d.ts.map +1 -1
  96. package/dist/esm/pagination-controls.js +28 -2
  97. package/dist/esm/pagination-controls.js.map +1 -1
  98. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  99. package/dist/esm/pagination-plugin.js +138 -102
  100. package/dist/esm/pagination-plugin.js.map +1 -1
  101. package/dist/esm/range-control.d.ts +45 -0
  102. package/dist/esm/range-control.d.ts.map +1 -0
  103. package/dist/esm/range-control.js +137 -0
  104. package/dist/esm/range-control.js.map +1 -0
  105. package/dist/esm/row-order.d.ts +3 -2
  106. package/dist/esm/row-order.d.ts.map +1 -1
  107. package/dist/esm/row-order.js +5 -4
  108. package/dist/esm/row-order.js.map +1 -1
  109. package/dist/esm/search-input.d.ts +42 -0
  110. package/dist/esm/search-input.d.ts.map +1 -0
  111. package/dist/esm/search-input.js +129 -0
  112. package/dist/esm/search-input.js.map +1 -0
  113. package/dist/esm/transformations/booking-item.d.ts +24 -0
  114. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  115. package/dist/esm/transformations/booking-item.js +23 -0
  116. package/dist/esm/transformations/booking-item.js.map +1 -0
  117. package/dist/esm/transformations/index.d.ts +4 -2
  118. package/dist/esm/transformations/index.d.ts.map +1 -1
  119. package/dist/esm/transformations/index.js +14 -5
  120. package/dist/esm/transformations/index.js.map +1 -1
  121. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +5 -5
  123. package/src/count-effect.ts +31 -8
  124. package/src/fetchers/pg-client-import.ts +16 -0
  125. package/src/fetchers/postgresql.ts +15 -9
  126. package/src/fetchers/raw-query.ts +2 -1
  127. package/src/fetchers/teleport.ts +11 -8
  128. package/src/infinite-scroll.ts +240 -79
  129. package/src/list-sort-order.ts +34 -0
  130. package/src/pagination-controls.ts +26 -3
  131. package/src/pagination-plugin.ts +163 -135
  132. package/src/range-control.ts +207 -0
  133. package/src/row-order.ts +5 -4
  134. package/src/search-input.ts +190 -0
  135. package/src/transformations/booking-item.ts +71 -0
  136. package/src/transformations/index.ts +25 -2
@@ -37,10 +37,15 @@ import {
37
37
  import {
38
38
  convertControlToButton,
39
39
  dropInactivePaginationControls,
40
+ findAllPaginationControls,
40
41
  findPaginationControl,
42
+ PAGINATION_FOR_ATTR,
43
+ readJSXAttributeString,
41
44
  setJSXExpressionAttribute,
42
45
  type PaginationControlKind,
43
46
  } from './pagination-controls'
47
+ import { findRangeControls, wireRangeControl } from './range-control'
48
+ import { findAllSearchInputsInJSX, pairSearchInputs, wireSearchInput } from './search-input'
44
49
  import { buildCountFetchEffect, buildCountSeqDeclaration } from './count-effect'
45
50
  import {
46
51
  findAllSearchClearButtonsInJSX,
@@ -53,6 +58,7 @@ import {
53
58
  type NumberedPaginationVars,
54
59
  } from './numbered-pagination'
55
60
  import {
61
+ buildAccumulatingRequestMark,
56
62
  buildAccumulatingResponseHandler,
57
63
  buildInfiniteScrollDeclarations,
58
64
  buildSentinelElement,
@@ -154,6 +160,10 @@ interface DataSourceUsage {
154
160
  dataSourceIdentifier: string
155
161
  // The cms-list-repeater renderPropIdentifier (e.g., "context_1i871")
156
162
  arrayMapperRenderProp: string
163
+ // The array mapper's editor node id (`nodeId` on the cms-list-repeater), which
164
+ // a `range` control placed outside the pagination container names in its
165
+ // `data-tq-pagination-for` attribute. Absent from UIDL exported before it.
166
+ nodeId?: string
157
167
  // Resource definition from UIDL
158
168
  resourceDefinition: {
159
169
  dataSourceId: string
@@ -603,6 +613,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
603
613
  index: index++,
604
614
  dataSourceIdentifier: parentDataSource.identifier,
605
615
  arrayMapperRenderProp,
616
+ nodeId: typeof content.nodeId === 'string' ? content.nodeId : undefined,
606
617
  resourceDefinition: {
607
618
  dataSourceId: parentDataSource.resourceDef.dataSourceId || '',
608
619
  tableName: parentDataSource.resourceDef.tableName || '',
@@ -704,6 +715,9 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
704
715
  setPageStateVar: string
705
716
  maxPagesStateVar: string
706
717
  setMaxPagesStateVar: string
718
+ /** The counted row total behind a `range` control; `null` until counted. */
719
+ totalStateVar: string
720
+ setTotalStateVar: string
707
721
  searchQueryVar: string
708
722
  setSearchQueryVar: string
709
723
  debouncedSearchQueryVar: string
@@ -722,6 +736,8 @@ function getStateVarsForUsage(usage: DataSourceUsage): {
722
736
  setPageStateVar: `setDs_${idx}_page`,
723
737
  maxPagesStateVar: `ds_${idx}_maxPages`,
724
738
  setMaxPagesStateVar: `setDs_${idx}_maxPages`,
739
+ totalStateVar: `ds_${idx}_total`,
740
+ setTotalStateVar: `setDs_${idx}_total`,
725
741
  searchQueryVar: `ds_${idx}_searchQuery`,
726
742
  setSearchQueryVar: `setDs_${idx}_searchQuery`,
727
743
  debouncedSearchQueryVar: `ds_${idx}_debouncedQuery`,
@@ -829,7 +845,8 @@ function pushSearchUrlSyncEffects(
829
845
  )
830
846
  // read-back: `?<searchUrlParamKey>=` → the input AND the applied query at once
831
847
  // (`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.
848
+ // debounce and keeps a page the address carries — `?searchKeyword=x&page=3`
849
+ // lands on page 3.
833
850
  effectStatements.push(
834
851
  URLSearchParamSync.buildUrlReadBackEffect(
835
852
  usage.searchUrlParamKey,
@@ -842,19 +859,26 @@ function pushSearchUrlSyncEffects(
842
859
 
843
860
  // `const ds_N_adoptUrlSearch = (update) => { … }`: the setter the search's URL
844
861
  // 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.
862
+ // applied (debounced) query together. A list whose page is in the address
863
+ // keeps its page — the page sync sets it from the same address. Any other
864
+ // list starts the new query on page 1, as a typed change does in the debounce
865
+ // effect: an appending list has usually advanced past page 1 by the time the
866
+ // router is ready, and the query's first answer would otherwise be its page 2
867
+ // — empty for a query only a few rows match.
847
868
  function buildUrlSearchAdopter(
848
869
  adopterVar: string,
849
870
  usage: DataSourceUsage,
850
871
  vars: ReturnType<typeof getStateVarsForUsage>
851
872
  ): types.VariableDeclaration {
852
873
  const update = types.identifier('update')
874
+ const pageReset = usageHasPageUrlSync(usage)
875
+ ? []
876
+ : [types.objectProperty(types.identifier('page'), types.numericLiteral(1))]
853
877
  const applyToDebounced =
854
878
  usage.category === 'paginated+search'
855
879
  ? // setDs_N_state((state) => {
856
880
  // const next = update(state.debouncedQuery)
857
- // return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next }
881
+ // return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next }
858
882
  // })
859
883
  types.callExpression(types.identifier(vars.setCombinedStateVar), [
860
884
  types.arrowFunctionExpression(
@@ -884,6 +908,7 @@ function buildUrlSearchAdopter(
884
908
  types.identifier('state'),
885
909
  types.objectExpression([
886
910
  types.spreadElement(types.identifier('state')),
911
+ ...pageReset,
887
912
  types.objectProperty(
888
913
  types.identifier('debouncedQuery'),
889
914
  types.identifier('next')
@@ -1186,6 +1211,14 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1186
1211
  }
1187
1212
  const blockStatement = arrowFunction.body as types.BlockStatement
1188
1213
 
1214
+ // The "Showing 1–8 of 12" texts. Known up front because a component that
1215
+ // draws one needs the counted TOTAL in state — `ds_N_maxPages` alone cannot
1216
+ // say how many rows there are — and that state, with the live count that
1217
+ // fills it, is declared per usage below. A component without one emits
1218
+ // exactly what it always did.
1219
+ const rangeControls = findRangeControls(blockStatement)
1220
+ const hasRangeControl = rangeControls.length > 0
1221
+
1189
1222
  // STEP 1: Build state registry from UIDL
1190
1223
  const registry = buildStateRegistry(uidl.node)
1191
1224
 
@@ -1500,12 +1533,16 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1500
1533
  pushPropIdsAsDeps(countEffectDeps, countEffectSeen, usage.filterPropIds)
1501
1534
  // Same goes for URL-driven filters (already documented above).
1502
1535
  pushUrlSearchParamMemoDeps(countEffectDeps, usage)
1536
+ if (hasRangeControl) {
1537
+ stateDeclarations.push(buildTotalDeclaration(vars))
1538
+ }
1503
1539
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar))
1504
1540
  effectStatements.push(
1505
1541
  buildCountFetchEffect({
1506
1542
  fileName,
1507
1543
  perPage: usage.perPage,
1508
1544
  setMaxPagesVar: vars.setMaxPagesStateVar,
1545
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
1509
1546
  urlParams,
1510
1547
  deps: countEffectDeps,
1511
1548
  countSeqRefVar: vars.countSeqRefVar,
@@ -1572,8 +1609,10 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1572
1609
  // "Next" button, but not to label a numbered strip or to know when an
1573
1610
  // infinite list has reached its end, so those two modes fetch a live
1574
1611
  // count as well.
1612
+ // A range text needs the live count too: the build-time count only
1613
+ // ever became a page count, never the total it shows.
1575
1614
  const needsLiveCount =
1576
- !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll
1615
+ !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll || hasRangeControl
1577
1616
  if (needsLiveCount) {
1578
1617
  const fileName = generateSafeFileName(
1579
1618
  usage.resourceDefinition.dataSourceType,
@@ -1610,12 +1649,16 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1610
1649
  pushPropIdsAsDeps(countDeps, countDepsSeen, usage.filterPropIds)
1611
1650
  pushUrlSearchParamMemoDeps(countDeps, usage)
1612
1651
 
1652
+ if (hasRangeControl) {
1653
+ stateDeclarations.push(buildTotalDeclaration(vars))
1654
+ }
1613
1655
  stateDeclarations.push(buildCountSeqDeclaration(vars.countSeqRefVar))
1614
1656
  effectStatements.push(
1615
1657
  buildCountFetchEffect({
1616
1658
  fileName,
1617
1659
  perPage: usage.perPage,
1618
1660
  setMaxPagesVar: vars.setMaxPagesStateVar,
1661
+ setTotalVar: hasRangeControl ? vars.setTotalStateVar : undefined,
1619
1662
  urlParams: countUrlParams,
1620
1663
  deps: countDeps,
1621
1664
  countSeqRefVar: vars.countSeqRefVar,
@@ -1868,10 +1911,8 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1868
1911
  // An auto-loading list needs something at the end of the rows for its
1869
1912
  // IntersectionObserver to watch.
1870
1913
  if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
1871
- insertSentinelAfterProvider(
1872
- blockStatement,
1873
- dp,
1874
- buildSentinelElement(getInfiniteScrollVars(usage.index))
1914
+ insertSentinelAfterProvider(blockStatement, dp, (parentTag) =>
1915
+ buildSentinelElement(getInfiniteScrollVars(usage.index), parentTag)
1875
1916
  )
1876
1917
  }
1877
1918
 
@@ -1940,25 +1981,17 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1940
1981
  stabilizeDataProviderWithoutRepeater(dp)
1941
1982
  })
1942
1983
 
1943
- // STEP 4: Wire search inputs
1944
- // Match search inputs to usages by order (within each dataSourceIdentifier that has search enabled)
1984
+ // STEP 4: Wire search inputs — a platform input by its place among the
1985
+ // search-enabled mappers, a page's own marked input by the mapper it names
1986
+ // (`search-input.ts`).
1945
1987
  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
1988
+ const usageBySearchInput = pairSearchInputs(searchInputs, registry.usages)
1989
+ for (const input of searchInputs) {
1990
+ const usage = usageBySearchInput.get(input.node)
1991
+ if (usage) {
1992
+ wireSearchInput(input, getStateVarsForUsage(usage))
1955
1993
  }
1956
-
1957
- const usage = searchEnabledUsages[idx]
1958
- const vars = getStateVarsForUsage(usage)
1959
- wireSearchInput(input.node, vars)
1960
- usageBySearchInput.set(input.node, usage)
1961
- })
1994
+ }
1962
1995
 
1963
1996
  // STEP 4.5: Wire the clear buttons, each to the input it shares a parent with.
1964
1997
  findAllSearchClearButtonsInJSX(blockStatement).forEach((button) => {
@@ -1997,6 +2030,25 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1997
2030
  }
1998
2031
  )
1999
2032
 
2033
+ // STEP 5b: Wire the range texts that name their mapper.
2034
+ //
2035
+ // A `range` control carrying `data-tq-pagination-for` belongs to the mapper
2036
+ // whose editor node id it names, wherever it sits — a template's footer or
2037
+ // toolbar is laid out by the author, not inside a pagination container, so
2038
+ // the container-ownership rule above cannot reach it. One without the
2039
+ // attribute was wired by its container in STEP 5; one naming a mapper this
2040
+ // component does not render stays as drawn.
2041
+ rangeControls.forEach((control) => {
2042
+ const ownerNodeId = readJSXAttributeString(control, PAGINATION_FOR_ATTR)
2043
+ if (!ownerNodeId) {
2044
+ return
2045
+ }
2046
+ const owner = registry.usages.find((usage) => usage.nodeId === ownerNodeId)
2047
+ if (owner) {
2048
+ wireRangeControlFor(control, owner, getStateVarsForUsage(owner))
2049
+ }
2050
+ })
2051
+
2000
2052
  // STEP 6: Update getStaticProps if this is a page
2001
2053
  if (isPage) {
2002
2054
  updateGetStaticProps(
@@ -2023,6 +2075,8 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
2023
2075
  * It has to be a sibling: the provider's own child is a render-prop function,
2024
2076
  * and anything placed inside it would be re-created on every fetch — the
2025
2077
  * observer would then be watching an element that is no longer in the document.
2078
+ * The sentinel is built for the element it lands in (`buildSentinelElement`):
2079
+ * a fragment is transparent, a component's children may go anywhere.
2026
2080
  *
2027
2081
  * Returns false when the provider has no element parent to splice into (a
2028
2082
  * provider returned directly from the component), leaving the list on its
@@ -2031,35 +2085,53 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
2031
2085
  function insertSentinelAfterProvider(
2032
2086
  blockStatement: types.BlockStatement,
2033
2087
  provider: any,
2034
- sentinel: types.JSXElement
2088
+ buildSentinel: (parentTag: string | null) => types.JSXElement
2035
2089
  ): boolean {
2036
2090
  let inserted = false
2037
2091
 
2038
- const traverse = (node: any): void => {
2092
+ const elementTagOf = (node: any, enclosing: string | null): string | null => {
2093
+ if (node?.type === 'JSXFragment') {
2094
+ return enclosing
2095
+ }
2096
+ if (node?.type !== 'JSXElement') {
2097
+ return enclosing
2098
+ }
2099
+ const name = node.openingElement?.name
2100
+ if (name?.type !== 'JSXIdentifier') {
2101
+ return null
2102
+ }
2103
+ if (name.name === 'Fragment') {
2104
+ return enclosing
2105
+ }
2106
+ return /^[a-z]/.test(name.name) ? name.name : null
2107
+ }
2108
+
2109
+ const traverse = (node: any, enclosing: string | null): void => {
2039
2110
  if (!node || inserted) {
2040
2111
  return
2041
2112
  }
2113
+ const tag = elementTagOf(node, enclosing)
2042
2114
  if (Array.isArray(node.children)) {
2043
2115
  const at = node.children.indexOf(provider)
2044
2116
  if (at !== -1) {
2045
- node.children.splice(at + 1, 0, sentinel)
2117
+ node.children.splice(at + 1, 0, buildSentinel(tag))
2046
2118
  inserted = true
2047
2119
  return
2048
2120
  }
2049
- node.children.forEach(traverse)
2121
+ node.children.forEach((child: any) => traverse(child, tag))
2050
2122
  }
2051
2123
  if (node.body) {
2052
2124
  if (Array.isArray(node.body)) {
2053
- node.body.forEach(traverse)
2125
+ node.body.forEach((child: any) => traverse(child, tag))
2054
2126
  } else {
2055
- traverse(node.body)
2127
+ traverse(node.body, tag)
2056
2128
  }
2057
2129
  }
2058
2130
  if (node.consequent) {
2059
- traverse(node.consequent)
2131
+ traverse(node.consequent, tag)
2060
2132
  }
2061
2133
  if (node.alternate) {
2062
- traverse(node.alternate)
2134
+ traverse(node.alternate, tag)
2063
2135
  }
2064
2136
  // `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
2065
2137
  // panel, a gated section: the provider sits on the RIGHT of a logical
@@ -2067,23 +2139,23 @@ function insertSentinelAfterProvider(
2067
2139
  // an argument. Left unvisited, the products list under the first tab of
2068
2140
  // a shop was never paginated or seeded, and rendered nothing.
2069
2141
  if (node.left) {
2070
- traverse(node.left)
2142
+ traverse(node.left, tag)
2071
2143
  }
2072
2144
  if (node.right) {
2073
- traverse(node.right)
2145
+ traverse(node.right, tag)
2074
2146
  }
2075
2147
  if (node.test) {
2076
- traverse(node.test)
2148
+ traverse(node.test, tag)
2077
2149
  }
2078
2150
  if (node.expression) {
2079
- traverse(node.expression)
2151
+ traverse(node.expression, tag)
2080
2152
  }
2081
2153
  if (node.argument) {
2082
- traverse(node.argument)
2154
+ traverse(node.argument, tag)
2083
2155
  }
2084
2156
  }
2085
2157
 
2086
- traverse(blockStatement)
2158
+ traverse(blockStatement, null)
2087
2159
  return inserted
2088
2160
  }
2089
2161
 
@@ -2199,68 +2271,6 @@ function findArrayMapperRenderPropInDataProvider(dataProvider: any): string | un
2199
2271
  return renderItemAttr.value.expression.params[0].name
2200
2272
  }
2201
2273
 
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
2274
  /**
2265
2275
  * Every pagination widget in the JSX, paired with the mapper it belongs to.
2266
2276
  *
@@ -3057,11 +3067,21 @@ function createFetchDataAttribute(
3057
3067
  [responseTail]
3058
3068
  )
3059
3069
 
3070
+ // An appending list records the request it now stands on before making it
3071
+ // (see `buildAccumulatingRequestMark`).
3060
3072
  const arrow = cache
3061
3073
  ? types.arrowFunctionExpression(
3062
3074
  [types.identifier('params')],
3063
3075
  buildCachedFetchBody(networkChain, cache)
3064
3076
  )
3077
+ : accumulate
3078
+ ? types.arrowFunctionExpression(
3079
+ [types.identifier('params')],
3080
+ types.blockStatement([
3081
+ buildAccumulatingRequestMark(getInfiniteScrollVars(accumulate.usage.index)),
3082
+ types.returnStatement(networkChain),
3083
+ ])
3084
+ )
3065
3085
  : types.arrowFunctionExpression([types.identifier('params')], networkChain)
3066
3086
 
3067
3087
  return types.jsxAttribute(
@@ -3072,39 +3092,6 @@ function createFetchDataAttribute(
3072
3092
  )
3073
3093
  }
3074
3094
 
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
3095
  /**
3109
3096
  * The `fetchData` attribute for one paginated usage.
3110
3097
  *
@@ -3120,6 +3107,41 @@ function buildFetchDataForUsage(fileName: string, usage: DataSourceUsage): types
3120
3107
  return createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
3121
3108
  }
3122
3109
 
3110
+ /** `const [ds_N_total, setDs_N_total] = useState(null)` — declared only beside a range control. */
3111
+ function buildTotalDeclaration(vars: ReturnType<typeof getStateVarsForUsage>): types.Statement {
3112
+ return types.variableDeclaration('const', [
3113
+ types.variableDeclarator(
3114
+ types.arrayPattern([
3115
+ types.identifier(vars.totalStateVar),
3116
+ types.identifier(vars.setTotalStateVar),
3117
+ ]),
3118
+ types.callExpression(types.identifier('useState'), [types.nullLiteral()])
3119
+ ),
3120
+ ])
3121
+ }
3122
+
3123
+ /**
3124
+ * Fills one `range` text with the live row range of `usage` (see
3125
+ * `range-control.ts`). A mapper that does not page has no count to show, so
3126
+ * its range text is left as drawn.
3127
+ */
3128
+ function wireRangeControlFor(
3129
+ control: any,
3130
+ usage: DataSourceUsage,
3131
+ vars: ReturnType<typeof getStateVarsForUsage>
3132
+ ): void {
3133
+ if (!usage.paginated) {
3134
+ return
3135
+ }
3136
+ const { currentPageExpr } = getPageAccessors(usage, vars)
3137
+ wireRangeControl(control, {
3138
+ totalVar: vars.totalStateVar,
3139
+ currentPageExpr: currentPageExpr(),
3140
+ perPage: usage.perPage,
3141
+ infiniteScroll: usage.infiniteScroll,
3142
+ })
3143
+ }
3144
+
3123
3145
  /**
3124
3146
  * Wires the authored Load More button of an appending list.
3125
3147
  *
@@ -3185,6 +3207,12 @@ function wirePaginationWidget(
3185
3207
  vars: ReturnType<typeof getStateVarsForUsage>,
3186
3208
  hasLoadingState: boolean
3187
3209
  ): void {
3210
+ // A range text inside the container takes the container's owner, like the
3211
+ // buttons beside it; one naming its own mapper is wired in STEP 5b instead.
3212
+ findAllPaginationControls(paginationNode, 'range')
3213
+ .filter((control) => !readJSXAttributeString(control, PAGINATION_FOR_ATTR))
3214
+ .forEach((control) => wireRangeControlFor(control, usage, vars))
3215
+
3188
3216
  if (usage.infiniteScroll) {
3189
3217
  // Only the Load More button survives; page controls make no sense for a
3190
3218
  // list the visitor never leaves.