@teleporthq/teleport-plugin-next-data-source 0.43.57 → 0.43.60

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 (147) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +230 -0
  2. package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
  3. package/__tests__/ecommerce-product-discounts.test.ts +196 -0
  4. package/__tests__/ecommerce-product-model.test.ts +179 -0
  5. package/__tests__/infinite-scroll.test.ts +143 -0
  6. package/__tests__/numbered-pagination.test.ts +113 -0
  7. package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
  8. package/__tests__/page-url-sync.test.ts +208 -0
  9. package/__tests__/product-price-sort.ts +313 -0
  10. package/__tests__/search-url-sync.test.ts +7 -3
  11. package/dist/cjs/array-mapper-pagination.d.ts +9 -0
  12. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
  13. package/dist/cjs/array-mapper-pagination.js.map +1 -1
  14. package/dist/cjs/count-effect.d.ts +42 -0
  15. package/dist/cjs/count-effect.d.ts.map +1 -0
  16. package/dist/cjs/count-effect.js +81 -0
  17. package/dist/cjs/count-effect.js.map +1 -0
  18. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/postgresql.js +2 -1
  20. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  21. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/teleport.js +3 -2
  23. package/dist/cjs/fetchers/teleport.js.map +1 -1
  24. package/dist/cjs/infinite-scroll.d.ts +65 -0
  25. package/dist/cjs/infinite-scroll.d.ts.map +1 -0
  26. package/dist/cjs/infinite-scroll.js +190 -0
  27. package/dist/cjs/infinite-scroll.js.map +1 -0
  28. package/dist/cjs/numbered-pagination.d.ts +42 -0
  29. package/dist/cjs/numbered-pagination.d.ts.map +1 -0
  30. package/dist/cjs/numbered-pagination.js +167 -0
  31. package/dist/cjs/numbered-pagination.js.map +1 -0
  32. package/dist/cjs/page-reset.d.ts +42 -0
  33. package/dist/cjs/page-reset.d.ts.map +1 -0
  34. package/dist/cjs/page-reset.js +116 -0
  35. package/dist/cjs/page-reset.js.map +1 -0
  36. package/dist/cjs/page-url-sync.d.ts +42 -0
  37. package/dist/cjs/page-url-sync.d.ts.map +1 -0
  38. package/dist/cjs/page-url-sync.js +180 -0
  39. package/dist/cjs/page-url-sync.js.map +1 -0
  40. package/dist/cjs/pagination-controls.d.ts +55 -0
  41. package/dist/cjs/pagination-controls.d.ts.map +1 -0
  42. package/dist/cjs/pagination-controls.js +159 -0
  43. package/dist/cjs/pagination-controls.js.map +1 -0
  44. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  45. package/dist/cjs/pagination-plugin.js +506 -160
  46. package/dist/cjs/pagination-plugin.js.map +1 -1
  47. package/dist/cjs/product-price-sort.d.ts +116 -0
  48. package/dist/cjs/product-price-sort.d.ts.map +1 -0
  49. package/dist/cjs/product-price-sort.js +169 -0
  50. package/dist/cjs/product-price-sort.js.map +1 -0
  51. package/dist/cjs/transformations/ecommerce-product.d.ts +12 -0
  52. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  53. package/dist/cjs/transformations/ecommerce-product.js +2 -1
  54. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  55. package/dist/cjs/transformations/index.d.ts +2 -1
  56. package/dist/cjs/transformations/index.d.ts.map +1 -1
  57. package/dist/cjs/transformations/index.js +17 -1
  58. package/dist/cjs/transformations/index.js.map +1 -1
  59. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  60. package/dist/cjs/transformations/shared-utils.js +1 -1
  61. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  62. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  63. package/dist/esm/array-mapper-pagination.d.ts +9 -0
  64. package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
  65. package/dist/esm/array-mapper-pagination.js.map +1 -1
  66. package/dist/esm/count-effect.d.ts +42 -0
  67. package/dist/esm/count-effect.d.ts.map +1 -0
  68. package/dist/esm/count-effect.js +53 -0
  69. package/dist/esm/count-effect.js.map +1 -0
  70. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  71. package/dist/esm/fetchers/postgresql.js +2 -1
  72. package/dist/esm/fetchers/postgresql.js.map +1 -1
  73. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  74. package/dist/esm/fetchers/teleport.js +3 -2
  75. package/dist/esm/fetchers/teleport.js.map +1 -1
  76. package/dist/esm/infinite-scroll.d.ts +65 -0
  77. package/dist/esm/infinite-scroll.d.ts.map +1 -0
  78. package/dist/esm/infinite-scroll.js +159 -0
  79. package/dist/esm/infinite-scroll.js.map +1 -0
  80. package/dist/esm/numbered-pagination.d.ts +42 -0
  81. package/dist/esm/numbered-pagination.d.ts.map +1 -0
  82. package/dist/esm/numbered-pagination.js +138 -0
  83. package/dist/esm/numbered-pagination.js.map +1 -0
  84. package/dist/esm/page-reset.d.ts +42 -0
  85. package/dist/esm/page-reset.d.ts.map +1 -0
  86. package/dist/esm/page-reset.js +87 -0
  87. package/dist/esm/page-reset.js.map +1 -0
  88. package/dist/esm/page-url-sync.d.ts +42 -0
  89. package/dist/esm/page-url-sync.d.ts.map +1 -0
  90. package/dist/esm/page-url-sync.js +152 -0
  91. package/dist/esm/page-url-sync.js.map +1 -0
  92. package/dist/esm/pagination-controls.d.ts +55 -0
  93. package/dist/esm/pagination-controls.d.ts.map +1 -0
  94. package/dist/esm/pagination-controls.js +126 -0
  95. package/dist/esm/pagination-controls.js.map +1 -0
  96. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  97. package/dist/esm/pagination-plugin.js +507 -161
  98. package/dist/esm/pagination-plugin.js.map +1 -1
  99. package/dist/esm/product-price-sort.d.ts +116 -0
  100. package/dist/esm/product-price-sort.d.ts.map +1 -0
  101. package/dist/esm/product-price-sort.js +160 -0
  102. package/dist/esm/product-price-sort.js.map +1 -0
  103. package/dist/esm/transformations/ecommerce-product.d.ts +12 -0
  104. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  105. package/dist/esm/transformations/ecommerce-product.js +3 -2
  106. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  107. package/dist/esm/transformations/index.d.ts +2 -1
  108. package/dist/esm/transformations/index.d.ts.map +1 -1
  109. package/dist/esm/transformations/index.js +17 -1
  110. package/dist/esm/transformations/index.js.map +1 -1
  111. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  112. package/dist/esm/transformations/shared-utils.js +1 -1
  113. package/dist/esm/transformations/shared-utils.js.map +1 -1
  114. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +5 -5
  116. package/src/array-mapper-pagination.ts +10 -0
  117. package/src/count-effect.ts +137 -0
  118. package/src/fetchers/postgresql.ts +52 -13
  119. package/src/fetchers/teleport.ts +53 -13
  120. package/src/infinite-scroll.ts +401 -0
  121. package/src/numbered-pagination.ts +292 -0
  122. package/src/page-reset.ts +162 -0
  123. package/src/page-url-sync.ts +241 -0
  124. package/src/pagination-controls.ts +176 -0
  125. package/src/pagination-plugin.ts +755 -346
  126. package/src/product-price-sort.ts +204 -0
  127. package/src/transformations/ecommerce-product.ts +141 -10
  128. package/src/transformations/index.ts +19 -1
  129. package/src/transformations/shared-utils.ts +14 -0
  130. package/dist/cjs/array-mapper-registry.d.ts +0 -85
  131. package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
  132. package/dist/cjs/array-mapper-registry.js +0 -296
  133. package/dist/cjs/array-mapper-registry.js.map +0 -1
  134. package/dist/cjs/pagination-with-count.d.ts +0 -6
  135. package/dist/cjs/pagination-with-count.d.ts.map +0 -1
  136. package/dist/cjs/pagination-with-count.js +0 -63
  137. package/dist/cjs/pagination-with-count.js.map +0 -1
  138. package/dist/esm/array-mapper-registry.d.ts +0 -85
  139. package/dist/esm/array-mapper-registry.d.ts.map +0 -1
  140. package/dist/esm/array-mapper-registry.js +0 -292
  141. package/dist/esm/array-mapper-registry.js.map +0 -1
  142. package/dist/esm/pagination-with-count.d.ts +0 -6
  143. package/dist/esm/pagination-with-count.d.ts.map +0 -1
  144. package/dist/esm/pagination-with-count.js +0 -34
  145. package/dist/esm/pagination-with-count.js.map +0 -1
  146. package/src/array-mapper-registry.ts +0 -416
  147. package/src/pagination-with-count.ts +0 -89
@@ -58,6 +58,15 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
58
58
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
59
59
  }
60
60
  };
61
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
62
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
63
+ if (ar || !(i in from)) {
64
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
65
+ ar[i] = from[i];
66
+ }
67
+ }
68
+ return to.concat(ar || Array.prototype.slice.call(from));
69
+ };
61
70
  Object.defineProperty(exports, "__esModule", { value: true });
62
71
  exports.createNextArrayMapperPaginationPlugin = void 0;
63
72
  var teleport_types_1 = require("@teleporthq/teleport-types");
@@ -73,6 +82,12 @@ var filter_utils_1 = require("./filter-utils");
73
82
  var loading_state_1 = require("./loading-state");
74
83
  var config_1 = require("./cache/config");
75
84
  var ast_1 = require("./cache/ast");
85
+ var pagination_controls_1 = require("./pagination-controls");
86
+ var count_effect_1 = require("./count-effect");
87
+ var numbered_pagination_1 = require("./numbered-pagination");
88
+ var infinite_scroll_1 = require("./infinite-scroll");
89
+ var page_reset_1 = require("./page-reset");
90
+ var page_url_sync_1 = require("./page-url-sync");
76
91
  // tslint:disable-next-line:no-any
77
92
  function parseSearchDefaultValue(raw) {
78
93
  if (!raw || typeof raw !== 'object') {
@@ -426,6 +441,17 @@ function buildStateRegistry(uidlNode) {
426
441
  content.searchUrlParamKey.trim().length > 0
427
442
  ? content.searchUrlParamKey.trim()
428
443
  : undefined;
444
+ // An accumulating list has no discrete pages, so it can neither jump
445
+ // between them nor put one in the URL. Normalising here — rather than at
446
+ // each of the ~6 emit sites — keeps every downstream branch able to trust
447
+ // `paginationMode` / `pageUrlParamKey` on their own.
448
+ var infiniteScroll = !!content.infiniteScroll;
449
+ var pageUrlParamKey = !infiniteScroll &&
450
+ typeof content.pageUrlParamKey === 'string' &&
451
+ content.pageUrlParamKey.trim().length > 0
452
+ ? content.pageUrlParamKey.trim()
453
+ : undefined;
454
+ var paginationMode = !infiniteScroll && content.paginationMode === 'numbered' ? 'numbered' : 'buttons';
429
455
  var usage = {
430
456
  index: index++,
431
457
  dataSourceIdentifier: parentDataSource.identifier,
@@ -449,6 +475,10 @@ function buildStateRegistry(uidlNode) {
449
475
  filterPropIds: filterPropIds,
450
476
  filterUrlSearchParamKeys: filterUrlSearchParamKeys,
451
477
  cache: (0, config_1.resolveCacheFromRepeaterContent)(content.cache, parentDataSource.resourceDef),
478
+ paginationMode: paginationMode,
479
+ infiniteScroll: infiniteScroll,
480
+ infiniteScrollLoadMore: infiniteScroll && !!content.infiniteScrollLoadMore,
481
+ pageUrlParamKey: pageUrlParamKey,
452
482
  category: 'plain',
453
483
  };
454
484
  // Determine category
@@ -540,6 +570,16 @@ function usageHasSearchUrlSync(usage) {
540
570
  return (!!usage.searchUrlParamKey &&
541
571
  (usage.category === 'paginated+search' || usage.category === 'search-only'));
542
572
  }
573
+ // The page ⇄ URL counterpart of `usageHasSearchUrlSync`: the usage must carry a
574
+ // `pageUrlParamKey` AND own a page state (only the paginated categories declare
575
+ // `ds_N_page` / `ds_N_state.page`). `infiniteScroll` usages are already excluded
576
+ // by the registry, which clears their key.
577
+ function usageHasPageUrlSync(usage) {
578
+ return (!!usage.pageUrlParamKey &&
579
+ usage.paginated &&
580
+ !usage.infiniteScroll &&
581
+ (usage.category === 'paginated+search' || usage.category === 'paginated-only'));
582
+ }
543
583
  // Emits the two URL-sync effects for a search input bound to a URL param
544
584
  // (`searchUrlParamKey`), reusing the shared `URLSearchParamSync` builders so the
545
585
  // search input behaves exactly like the `selectedCategory` / `sortBy` dropdowns:
@@ -579,10 +619,157 @@ function pushSearchUrlSyncEffects(effectStatements, usage, vars) {
579
619
  // debounce effect propagates the change into the debounced value + fetch.
580
620
  effectStatements.push(teleport_plugin_common_1.URLSearchParamSync.buildUrlReadBackEffect(usage.searchUrlParamKey, vars.setSearchQueryVar));
581
621
  }
622
+ // How one usage reads and writes its current page.
623
+ //
624
+ // A paginated+search list keeps the page inside a combined
625
+ // `{ page, debouncedQuery }` object so a search reset can replace both at once,
626
+ // while a paginated-only list has a plain `ds_N_page`. Every control that moves
627
+ // the page goes through this pair rather than re-deriving the distinction.
628
+ function getPageAccessors(usage, vars) {
629
+ var isCombinedState = usage.category === 'paginated+search';
630
+ return {
631
+ currentPageExpr: function () {
632
+ return isCombinedState
633
+ ? types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page'))
634
+ : types.identifier(vars.pageStateVar);
635
+ },
636
+ // Functional updates that bail when the page is unchanged: clicking the
637
+ // page you are already on must not schedule a render, and with it a refetch.
638
+ buildSetPageStatement: function (pageExpr) {
639
+ return isCombinedState
640
+ ? types.callExpression(types.identifier(vars.setCombinedStateVar), [
641
+ types.arrowFunctionExpression([types.identifier('state')], types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('state'), types.identifier('page')), types.cloneNode(pageExpr, true)), types.identifier('state'), types.objectExpression([
642
+ types.spreadElement(types.identifier('state')),
643
+ types.objectProperty(types.identifier('page'), types.cloneNode(pageExpr, true)),
644
+ ]))),
645
+ ])
646
+ : types.callExpression(types.identifier(vars.setPageStateVar), [
647
+ types.arrowFunctionExpression([types.identifier('page')], types.conditionalExpression(types.binaryExpression('===', types.identifier('page'), types.cloneNode(pageExpr, true)), types.identifier('page'), types.cloneNode(pageExpr, true))),
648
+ ]);
649
+ },
650
+ };
651
+ }
652
+ /**
653
+ * Advance one page, unless the count already says this is the last one. Shared
654
+ * by the scroll sentinel and the Load More button.
655
+ *
656
+ * ⛔ The next page is derived from the updater's OWN argument, never from the
657
+ * page in scope. The sentinel's effect is re-armed on `hasMore` / `maxPages`,
658
+ * and neither changes on an append while the total is unknown — so a captured
659
+ * `ds_N_page` would still read 1 on the second intersection and the list would
660
+ * stop growing after one page, on exactly the sources that cannot be counted.
661
+ */
662
+ function buildAdvancePageStatement(usage, vars) {
663
+ var isCombinedState = usage.category === 'paginated+search';
664
+ var paramName = isCombinedState ? 'state' : 'page';
665
+ var pageOfParam = function () {
666
+ return isCombinedState
667
+ ? types.memberExpression(types.identifier(paramName), types.identifier('page'))
668
+ : types.identifier(paramName);
669
+ };
670
+ // maxPages > 0 && <param page> >= maxPages
671
+ var atLastPage = types.logicalExpression('&&', types.binaryExpression('>', types.identifier(vars.maxPagesStateVar), types.numericLiteral(0)), types.binaryExpression('>=', pageOfParam(), types.identifier(vars.maxPagesStateVar)));
672
+ var nextPage = types.binaryExpression('+', pageOfParam(), types.numericLiteral(1));
673
+ return types.expressionStatement(types.callExpression(types.identifier(isCombinedState ? vars.setCombinedStateVar : vars.setPageStateVar), [
674
+ types.arrowFunctionExpression([types.identifier(paramName)], types.conditionalExpression(atLastPage, types.identifier(paramName), isCombinedState
675
+ ? types.objectExpression([
676
+ types.spreadElement(types.identifier(paramName)),
677
+ types.objectProperty(types.identifier('page'), nextPage),
678
+ ])
679
+ : nextPage)),
680
+ ]));
681
+ }
682
+ // The accumulator, the end-of-data flag and (in auto mode) the sentinel ref,
683
+ // declared only for a mapper that appends.
684
+ function pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars) {
685
+ if (!usage.infiniteScroll || !usage.paginated) {
686
+ return;
687
+ }
688
+ var infiniteVars = (0, infinite_scroll_1.getInfiniteScrollVars)(usage.index);
689
+ var currentPageExpr = getPageAccessors(usage, vars).currentPageExpr;
690
+ var usesSentinel = !usage.infiniteScrollLoadMore;
691
+ stateDeclarations.push.apply(stateDeclarations, (0, infinite_scroll_1.buildInfiniteScrollDeclarations)(infiniteVars, vars.maxPagesStateVar, currentPageExpr(), usesSentinel));
692
+ if (usesSentinel) {
693
+ effectStatements.push((0, infinite_scroll_1.buildSentinelObserverEffect)(infiniteVars, vars.maxPagesStateVar, function () {
694
+ return buildAdvancePageStatement(usage, vars);
695
+ }));
696
+ }
697
+ }
698
+ // The `useMemo` holding the visible page numbers, declared only for a mapper in
699
+ // numbered mode.
700
+ function pushPageTokensDeclaration(stateDeclarations, usage, vars) {
701
+ if (usage.paginationMode !== 'numbered' || !usage.paginated) {
702
+ return;
703
+ }
704
+ var currentPageExpr = getPageAccessors(usage, vars).currentPageExpr;
705
+ stateDeclarations.push((0, numbered_pagination_1.buildPageTokensDeclaration)(usage.index, vars.maxPagesStateVar, currentPageExpr()));
706
+ }
707
+ // Emits the page ⇄ `?<key>=` sync for a mapper bound to `pageUrlParamKey` — the
708
+ // pagination counterpart of `pushSearchUrlSyncEffects`, but a SINGLE effect
709
+ // that owns both directions. `page-url-sync.ts` explains at length why the
710
+ // usual read-back / write-back pair cannot be used here.
711
+ //
712
+ // ⛔ MUST be pushed AFTER the filter/sort reset effect. Both can fire in one
713
+ // flush on a navigation that changes filter and page together; effects run in
714
+ // emission order, so this one running LAST is what makes the URL win when it
715
+ // carries a page, while a plain user filter change (whose URL has not moved
716
+ // yet) is left with the reset's page 1 and writes that back.
717
+ function pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars) {
718
+ if (!usageHasPageUrlSync(usage)) {
719
+ return;
720
+ }
721
+ var refVar = "ds_".concat(usage.index, "_pageUrlRef");
722
+ var _a = getPageAccessors(usage, vars), currentPageExpr = _a.currentPageExpr, buildSetPageStatement = _a.buildSetPageStatement;
723
+ stateDeclarations.push((0, page_url_sync_1.buildPageUrlRefDeclaration)(refVar));
724
+ effectStatements.push((0, page_url_sync_1.buildPageUrlSyncEffect)({
725
+ paramKey: usage.pageUrlParamKey,
726
+ refVar: refVar,
727
+ currentPageExpr: currentPageExpr,
728
+ buildSetPageStatement: buildSetPageStatement,
729
+ }));
730
+ }
731
+ // The name of the ref holding this usage's last-seen filter/sort signature.
732
+ function getPageResetRefVar(usage) {
733
+ return "ds_".concat(usage.index, "_filterResetRef");
734
+ }
735
+ // Everything that can narrow this list: filter destinations (state-, prop- and
736
+ // URL-bound) plus the state a dynamic sort reads. Sort does not change WHICH
737
+ // rows match, but it does change which row is on which page, so holding the
738
+ // visitor on page 5 across a sort change shows them an arbitrary window of a
739
+ // list they just reordered.
740
+ function collectPageResetDeps(usage) {
741
+ var _a, _b;
742
+ return {
743
+ stateIds: __spreadArray(__spreadArray([], usage.filterStateIds, true), ((_b = (_a = usage.dynamicSort) === null || _a === void 0 ? void 0 : _a.depStateIds) !== null && _b !== void 0 ? _b : []), true),
744
+ propIds: usage.filterPropIds,
745
+ urlSearchParamKeys: usage.filterUrlSearchParamKeys,
746
+ };
747
+ }
748
+ // Emits the "a filter or sort changed ⇒ go back to page 1" effect for one
749
+ // paginated usage. No-op for a list nothing can filter or reorder, so a mapper
750
+ // without filters generates byte-for-byte what it always did.
751
+ function pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars) {
752
+ if (!usage.paginated) {
753
+ return;
754
+ }
755
+ var deps = collectPageResetDeps(usage);
756
+ if ((0, page_reset_1.hasNoPageResetDeps)(deps)) {
757
+ return;
758
+ }
759
+ var refVar = getPageResetRefVar(usage);
760
+ stateDeclarations.push((0, page_reset_1.buildPageResetRefDeclaration)(refVar));
761
+ // The setter bails when the page is already 1 (see `getPageAccessors`), so a
762
+ // visitor who was on page 1 all along and merely ticked a category does not
763
+ // get an extra render — and with it an extra fetch.
764
+ var buildSetPageStatement = getPageAccessors(usage, vars).buildSetPageStatement;
765
+ effectStatements.push((0, page_reset_1.buildFilterSortPageResetEffect)(refVar, deps, function () {
766
+ return types.expressionStatement(buildSetPageStatement(types.numericLiteral(1)));
767
+ }));
768
+ }
582
769
  // ==================== MAIN PLUGIN ====================
583
770
  var createNextArrayMapperPaginationPlugin = function () {
584
771
  var paginationPlugin = function (structure) { return __awaiter(void 0, void 0, void 0, function () {
585
- var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages, paginationNodes, paginatedUsages;
772
+ var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, usagesWithLoadingState, usageByProviderNode, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages;
586
773
  var _a, _b;
587
774
  return __generator(this, function (_c) {
588
775
  uidl = structure.uidl, chunks = structure.chunks, dependencies = structure.dependencies, options = structure.options;
@@ -714,11 +901,19 @@ var createNextArrayMapperPaginationPlugin = function () {
714
901
  types.variableDeclaration('const', [
715
902
  types.variableDeclarator(types.identifier('timer'), types.callExpression(types.identifier('setTimeout'), [
716
903
  types.arrowFunctionExpression([], types.blockStatement([
904
+ // ⛔ Functional, and a no-op when the query has not
905
+ // actually moved. This effect fires on ANY change to
906
+ // the input state — including the URL read-back
907
+ // echoing back a value the visitor never typed (a
908
+ // browser Forward to `?searchKeyword=x&page=3`) — and
909
+ // replacing the whole object would silently drop the
910
+ // page it had just adopted, as well as remount the
911
+ // provider on a `key` that contains the query.
717
912
  types.expressionStatement(types.callExpression(types.identifier(vars.setCombinedStateVar), [
718
- types.objectExpression([
913
+ types.arrowFunctionExpression([types.identifier('state')], types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('state'), types.identifier('debouncedQuery')), types.identifier(vars.searchQueryVar)), types.identifier('state'), types.objectExpression([
719
914
  types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
720
915
  types.objectProperty(types.identifier('debouncedQuery'), types.identifier(vars.searchQueryVar)),
721
- ]),
916
+ ]))),
722
917
  ])),
723
918
  ])),
724
919
  types.numericLiteral(usage.searchDebounce),
@@ -751,42 +946,6 @@ var createNextArrayMapperPaginationPlugin = function () {
751
946
  }
752
947
  // Add filters to count fetch params if present
753
948
  (0, filter_utils_1.appendFiltersParam)(urlParams, usage.filters, buildFilterDestinationExpression);
754
- // Build the count fetch effect body
755
- var countFetchEffectBody = [];
756
- // Refresh the count on mount (no skip-on-mount guard). Pages seed maxPages
757
- // from the build-time `getStaticProps` count, but that snapshot goes stale:
758
- // rows added AFTER the build (e.g. the normalized product_variants table,
759
- // which is seeded/grows past one page during store generation) are not
760
- // reflected, so a frozen maxPages of 0/1 leaves "Next" permanently disabled
761
- // on data-heavy tables. Re-fetching the live count on mount corrects
762
- // maxPages so pagination tracks the real row count. The effect still runs
763
- // when the search query or a filter changes (see deps below).
764
- // Add the fetch call
765
- countFetchEffectBody.push(types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
766
- types.templateLiteral([
767
- types.templateElement({
768
- raw: "/api/".concat(fileName, "-count?"),
769
- cooked: "/api/".concat(fileName, "-count?"),
770
- }),
771
- types.templateElement({ raw: '', cooked: '' }),
772
- ], [
773
- types.newExpression(types.identifier('URLSearchParams'), [
774
- types.objectExpression(urlParams),
775
- ]),
776
- ]),
777
- ]), types.identifier('then')), [
778
- types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
779
- ]), types.identifier('then')), [
780
- types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
781
- types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
782
- types.expressionStatement(types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
783
- types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(0)), types.numericLiteral(0), types.callExpression(types.memberExpression(types.identifier('Math'), types.identifier('ceil')), [
784
- types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(usage.perPage)),
785
- ])),
786
- ])),
787
- ])),
788
- ])),
789
- ])));
790
949
  var countEffectDeps = [
791
950
  types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
792
951
  ];
@@ -801,12 +960,24 @@ var createNextArrayMapperPaginationPlugin = function () {
801
960
  (0, filter_utils_1.pushPropIdsAsDeps)(countEffectDeps, countEffectSeen, usage.filterPropIds);
802
961
  // Same goes for URL-driven filters (already documented above).
803
962
  pushUrlSearchParamMemoDeps(countEffectDeps, usage);
804
- effectStatements.push(types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
805
- types.arrowFunctionExpression([], types.blockStatement(countFetchEffectBody)),
806
- types.arrayExpression(countEffectDeps),
807
- ])));
963
+ effectStatements.push((0, count_effect_1.buildCountFetchEffect)({
964
+ fileName: fileName,
965
+ perPage: usage.perPage,
966
+ setMaxPagesVar: vars.setMaxPagesStateVar,
967
+ urlParams: urlParams,
968
+ deps: countEffectDeps,
969
+ }));
808
970
  // Two-way URL sync for the search input when bound to a URL param.
809
971
  pushSearchUrlSyncEffects(effectStatements, usage, vars);
972
+ // Visible page numbers, when this mapper draws a numbered strip.
973
+ pushPageTokensDeclaration(stateDeclarations, usage, vars);
974
+ // Accumulation state, when it appends instead of paging.
975
+ pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars);
976
+ // A filter or sort change invalidates the page the visitor is on.
977
+ pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars);
978
+ // Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
979
+ // after the reset so a URL that names a page wins over it.
980
+ pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars);
810
981
  }
811
982
  else if (usage.category === 'paginated-only') {
812
983
  // Simple page state
@@ -825,42 +996,52 @@ var createNextArrayMapperPaginationPlugin = function () {
825
996
  types.identifier(vars.setPageStateVar),
826
997
  ]), types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])),
827
998
  ]));
828
- // For components (not pages), add a useEffect to fetch count on mount
829
- // Pages get count from getStaticProps, but components need to fetch it client-side
830
- if (!isPage) {
999
+ // The live row count behind `ds_N_maxPages`.
1000
+ //
1001
+ // Components have never had a build-time count to fall back on, so they
1002
+ // have always fetched one here. Pages seed theirs from `getStaticProps`,
1003
+ // which only knows the STATIC filters — good enough to enable/disable a
1004
+ // "Next" button, but not to label a numbered strip or to know when an
1005
+ // infinite list has reached its end, so those two modes fetch a live
1006
+ // count as well.
1007
+ var needsLiveCount = !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll;
1008
+ if (needsLiveCount) {
831
1009
  var fileName = (0, utils_1.generateSafeFileName)(usage.resourceDefinition.dataSourceType, usage.resourceDefinition.tableName, usage.resourceDefinition.dataSourceId);
832
- effectStatements.push(types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
833
- types.arrowFunctionExpression([], types.blockStatement([
834
- types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
835
- types.stringLiteral("/api/".concat(fileName, "-count")),
836
- ]), types.identifier('then')), [
837
- types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
838
- ]), types.identifier('then')), [
839
- types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
840
- types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
841
- types.expressionStatement(types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
842
- types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(0)), types.numericLiteral(0), types.callExpression(types.memberExpression(types.identifier('Math'), types.identifier('ceil')), [
843
- types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(usage.perPage)),
844
- ])),
845
- ])),
846
- ])),
847
- ])),
848
- ])),
849
- ])),
850
- // Default to mount-only; refresh when ANY filter destination
851
- // changes — state-bound (e.g. `selectedCategory`) and
852
- // URL-driven — so the pagination control reflects the current
853
- // filtered count instead of the unfiltered mount-time total.
854
- types.arrayExpression((function () {
855
- var deps = [];
856
- var depsSeen = new Set();
857
- (0, filter_utils_1.pushStateIdsAsDeps)(deps, depsSeen, usage.filterStateIds);
858
- (0, filter_utils_1.pushPropIdsAsDeps)(deps, depsSeen, usage.filterPropIds);
859
- pushUrlSearchParamMemoDeps(deps, usage);
860
- return deps;
861
- })()),
862
- ])));
1010
+ // ⛔ This list has no search input, so no `query` param — but it may
1011
+ // well have filters, and omitting them (as this call site did until
1012
+ // the shared builder replaced it) counts the WHOLE table: "Next"
1013
+ // stayed enabled far past the end of a filtered result set.
1014
+ var countUrlParams = [];
1015
+ if (usage.queryColumns.length > 0) {
1016
+ countUrlParams.push(types.objectProperty(types.identifier('queryColumns'), types.callExpression(types.memberExpression(types.identifier('JSON'), types.identifier('stringify')), [types.arrayExpression(usage.queryColumns.map(function (c) { return types.stringLiteral(c); }))])));
1017
+ }
1018
+ (0, filter_utils_1.appendFiltersParam)(countUrlParams, usage.filters, buildFilterDestinationExpression);
1019
+ // Default to mount-only; refresh when ANY filter destination
1020
+ // changes — state-bound (e.g. `selectedCategory`) and URL-driven —
1021
+ // so the pagination control reflects the current filtered count
1022
+ // instead of the unfiltered mount-time total.
1023
+ var countDeps = [];
1024
+ var countDepsSeen = new Set();
1025
+ (0, filter_utils_1.pushStateIdsAsDeps)(countDeps, countDepsSeen, usage.filterStateIds);
1026
+ (0, filter_utils_1.pushPropIdsAsDeps)(countDeps, countDepsSeen, usage.filterPropIds);
1027
+ pushUrlSearchParamMemoDeps(countDeps, usage);
1028
+ effectStatements.push((0, count_effect_1.buildCountFetchEffect)({
1029
+ fileName: fileName,
1030
+ perPage: usage.perPage,
1031
+ setMaxPagesVar: vars.setMaxPagesStateVar,
1032
+ urlParams: countUrlParams,
1033
+ deps: countDeps,
1034
+ }));
863
1035
  }
1036
+ // Visible page numbers, when this mapper draws a numbered strip.
1037
+ pushPageTokensDeclaration(stateDeclarations, usage, vars);
1038
+ // Accumulation state, when it appends instead of paging.
1039
+ pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars);
1040
+ // A filter or sort change invalidates the page the visitor is on.
1041
+ pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars);
1042
+ // Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
1043
+ // after the reset so a URL that names a page wins over it.
1044
+ pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars);
864
1045
  }
865
1046
  else if (usage.category === 'search-only') {
866
1047
  // Search-only state
@@ -906,7 +1087,7 @@ var createNextArrayMapperPaginationPlugin = function () {
906
1087
  });
907
1088
  // Insert state declarations at the beginning
908
1089
  stateDeclarations.reverse().forEach(function (s) { return blockStatement.body.unshift(s); });
909
- needsUseRouter = registry.usages.some(function (u) { return hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u); });
1090
+ needsUseRouter = registry.usages.some(function (u) { return hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u) || usageHasPageUrlSync(u); });
910
1091
  if (needsUseRouter) {
911
1092
  if (!dependencies.useRouter) {
912
1093
  // Match the shape the sibling Next.js plugins use (i18n locale mapper,
@@ -936,6 +1117,13 @@ var createNextArrayMapperPaginationPlugin = function () {
936
1117
  ]));
937
1118
  }
938
1119
  }
1120
+ // Both the search write-back and the page ⇄ URL effect write the URL through
1121
+ // the shared `URLQueryWriter`, so the ref + helper they close over must be in
1122
+ // the body. Idempotent with `createNextUrlSearchParamsPlugin`, which needs the
1123
+ // same pair for the category/sort dropdowns and may have run first.
1124
+ if (registry.usages.some(function (u) { return usageHasSearchUrlSync(u) || usageHasPageUrlSync(u); })) {
1125
+ teleport_plugin_common_1.URLQueryWriter.ensureQuerySyncDeclarations(blockStatement.body);
1126
+ }
939
1127
  returnIndex = blockStatement.body.findIndex(function (s) { return s.type === 'ReturnStatement'; });
940
1128
  insertIndex = returnIndex !== -1 ? returnIndex : blockStatement.body.length;
941
1129
  effectStatements.reverse().forEach(function (e) { return blockStatement.body.splice(insertIndex, 0, e); });
@@ -946,6 +1134,8 @@ var createNextArrayMapperPaginationPlugin = function () {
946
1134
  });
947
1135
  usageIndexByDataSourceId = new Map();
948
1136
  loadingStateDeclarations = [];
1137
+ usagesWithLoadingState = new Set();
1138
+ usageByProviderNode = new Map();
949
1139
  cacheDeclarations = [];
950
1140
  dataProvidersWithRepeaters.forEach(function (dp) {
951
1141
  var _a, _b;
@@ -962,6 +1152,7 @@ var createNextArrayMapperPaginationPlugin = function () {
962
1152
  }
963
1153
  var usage = usages[currentIndex];
964
1154
  usageIndexByDataSourceId.set(dataSourceIdentifier, currentIndex + 1);
1155
+ usageByProviderNode.set(dp, usage);
965
1156
  var vars = getStateVarsForUsage(usage);
966
1157
  var fileName = (0, utils_1.generateSafeFileName)(usage.resourceDefinition.dataSourceType, usage.resourceDefinition.tableName, usage.resourceDefinition.dataSourceId);
967
1158
  // Update DataProvider based on category
@@ -984,6 +1175,15 @@ var createNextArrayMapperPaginationPlugin = function () {
984
1175
  var loadingVars = (0, loading_state_1.getLoadingStateVars)(usage.index);
985
1176
  if ((0, loading_state_1.applyLoadingStateToDataProvider)(dp, loadingVars)) {
986
1177
  loadingStateDeclarations.push.apply(loadingStateDeclarations, (0, loading_state_1.buildLoadingStateDeclarations)(loadingVars));
1178
+ // Recorded because the loading state is only declared for a provider
1179
+ // that HAS a loading slot; a Load More button wired later may only
1180
+ // reference `ds_N_isFetching` if it actually exists.
1181
+ usagesWithLoadingState.add(usage.index);
1182
+ }
1183
+ // An auto-loading list needs something at the end of the rows for its
1184
+ // IntersectionObserver to watch.
1185
+ if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
1186
+ insertSentinelAfterProvider(blockStatement, dp, (0, infinite_scroll_1.buildSentinelElement)((0, infinite_scroll_1.getInfiniteScrollVars)(usage.index)));
987
1187
  }
988
1188
  // Create API route for all categories (including 'plain' for components)
989
1189
  ensureAPIRouteExists(options.extractedResources, usage, options.dataSources, (0, transformations_1.buildProductTransformOptions)(options));
@@ -1035,16 +1235,23 @@ var createNextArrayMapperPaginationPlugin = function () {
1035
1235
  var vars = getStateVarsForUsage(usage);
1036
1236
  wireSearchInput(input.node, vars);
1037
1237
  });
1038
- paginationNodes = findAllPaginationNodesInJSX(blockStatement);
1039
- paginatedUsages = registry.usages.filter(function (u) { return u.paginated; });
1040
- // Match by order - pagination node 0 -> paginatedUsages[0], etc.
1041
- paginationNodes.forEach(function (paginationNode, idx) {
1042
- if (idx >= paginatedUsages.length) {
1238
+ // STEP 5: Wire pagination widgets
1239
+ //
1240
+ // Each widget belongs to the mapper whose `DataProvider` it follows in the
1241
+ // document — the builder emits it as a sibling of (or inside) that provider.
1242
+ //
1243
+ // ⛔ Not a positional zip of "widgets" against "paginated usages": the two
1244
+ // lists no longer have to be the same length, because an infinite-scroll
1245
+ // mapper renders no widget. Counting one for it would shift every later
1246
+ // mapper onto the wrong usage and wire one list's buttons to another list's
1247
+ // state. Anchoring on the provider cannot drift for the same reason the
1248
+ // provider loop above cannot.
1249
+ findPaginationNodesWithOwners(blockStatement, usageByProviderNode).forEach(function (_a) {
1250
+ var node = _a.node, usage = _a.usage;
1251
+ if (!usage) {
1043
1252
  return;
1044
1253
  }
1045
- var usage = paginatedUsages[idx];
1046
- var vars = getStateVarsForUsage(usage);
1047
- wirePaginationButtons(paginationNode.node, usage, vars);
1254
+ wirePaginationWidget(node, usage, getStateVarsForUsage(usage), usagesWithLoadingState.has(usage.index));
1048
1255
  });
1049
1256
  // STEP 6: Update getStaticProps if this is a page
1050
1257
  if (isPage) {
@@ -1057,6 +1264,57 @@ var createNextArrayMapperPaginationPlugin = function () {
1057
1264
  };
1058
1265
  exports.createNextArrayMapperPaginationPlugin = createNextArrayMapperPaginationPlugin;
1059
1266
  // ==================== HELPER FUNCTIONS ====================
1267
+ /**
1268
+ * Inserts the scroll sentinel immediately after a `DataProvider`, as its next
1269
+ * sibling, so it sits below the rendered rows.
1270
+ *
1271
+ * It has to be a sibling: the provider's own child is a render-prop function,
1272
+ * and anything placed inside it would be re-created on every fetch — the
1273
+ * observer would then be watching an element that is no longer in the document.
1274
+ *
1275
+ * Returns false when the provider has no element parent to splice into (a
1276
+ * provider returned directly from the component), leaving the list on its
1277
+ * count-driven `hasMore` rather than injecting a stray node.
1278
+ */
1279
+ function insertSentinelAfterProvider(blockStatement, provider, sentinel) {
1280
+ var inserted = false;
1281
+ var traverse = function (node) {
1282
+ if (!node || inserted) {
1283
+ return;
1284
+ }
1285
+ if (Array.isArray(node.children)) {
1286
+ var at = node.children.indexOf(provider);
1287
+ if (at !== -1) {
1288
+ node.children.splice(at + 1, 0, sentinel);
1289
+ inserted = true;
1290
+ return;
1291
+ }
1292
+ node.children.forEach(traverse);
1293
+ }
1294
+ if (node.body) {
1295
+ if (Array.isArray(node.body)) {
1296
+ node.body.forEach(traverse);
1297
+ }
1298
+ else {
1299
+ traverse(node.body);
1300
+ }
1301
+ }
1302
+ if (node.consequent) {
1303
+ traverse(node.consequent);
1304
+ }
1305
+ if (node.alternate) {
1306
+ traverse(node.alternate);
1307
+ }
1308
+ if (node.expression) {
1309
+ traverse(node.expression);
1310
+ }
1311
+ if (node.argument) {
1312
+ traverse(node.argument);
1313
+ }
1314
+ };
1315
+ traverse(blockStatement);
1316
+ return inserted;
1317
+ }
1060
1318
  function findAllDataProvidersInJSX(blockStatement) {
1061
1319
  var results = [];
1062
1320
  var traverse = function (node) {
@@ -1176,18 +1434,32 @@ function findAllSearchInputsInJSX(blockStatement) {
1176
1434
  traverse(blockStatement);
1177
1435
  return results;
1178
1436
  }
1179
- function findAllPaginationNodesInJSX(blockStatement) {
1437
+ /**
1438
+ * Every pagination widget in the JSX, paired with the mapper it belongs to.
1439
+ *
1440
+ * Ownership follows document order: a widget belongs to the most recent
1441
+ * `DataProvider` seen before it, which is exactly how the builder emits it —
1442
+ * as a sibling that follows its provider, or nested inside that provider's
1443
+ * subtree. A widget that precedes every provider (nothing sensible to wire it
1444
+ * to) comes back with no owner and is left alone.
1445
+ */
1446
+ function findPaginationNodesWithOwners(blockStatement, usageByProviderNode) {
1180
1447
  var results = [];
1448
+ var currentUsage;
1181
1449
  var traverse = function (node) {
1182
1450
  var _a, _b, _c, _d, _e;
1183
1451
  if (!node) {
1184
1452
  return;
1185
1453
  }
1186
1454
  if (node.type === 'JSXElement') {
1455
+ var owner = usageByProviderNode.get(node);
1456
+ if (owner) {
1457
+ currentUsage = owner;
1458
+ }
1187
1459
  var classAttr = (_b = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'className'; });
1188
1460
  var className = ((_c = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _c === void 0 ? void 0 : _c.value) || ((_e = (_d = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _d === void 0 ? void 0 : _d.expression) === null || _e === void 0 ? void 0 : _e.value) || '';
1189
1461
  if (className.includes('cms-pagination-node')) {
1190
- results.push({ node: node, className: className });
1462
+ results.push({ node: node, usage: currentUsage });
1191
1463
  }
1192
1464
  }
1193
1465
  if (node.children && Array.isArray(node.children)) {
@@ -1250,7 +1522,6 @@ dp, usage, paramsProps, memoDeps, cacheDeclarations) {
1250
1522
  return hoisted.cachedIdentifier;
1251
1523
  }
1252
1524
  function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDeclarations) {
1253
- var _a;
1254
1525
  var attrs = dp.openingElement.attributes;
1255
1526
  // Remove existing params, key, initialData, fetchData, persistDataDuringLoading
1256
1527
  dp.openingElement.attributes = attrs.filter(function (attr) {
@@ -1286,7 +1557,12 @@ function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDe
1286
1557
  // the first client fetch (its internal guard only skips when initialData is
1287
1558
  // defined on mount). Without that skip, toggling sort correctly triggers a
1288
1559
  // refetch via the useMemo params dependency chain.
1289
- if (!usage.dynamicSort) {
1560
+ //
1561
+ // An appending list skips it for a second reason: `initialData` makes
1562
+ // DataProvider skip its first fetch, so page 1 would render from the prefetch
1563
+ // and never enter the accumulator — appending page 2 would then REPLACE the
1564
+ // visible rows with page 2 alone.
1565
+ if (!usage.dynamicSort && !usage.infiniteScroll) {
1290
1566
  var initialDataCondition = wrapInitialDataWithUrlFilterGuard(types.logicalExpression('&&', types.binaryExpression('===', types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')), types.numericLiteral(1)), types.unaryExpression('!', types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')), true)), usage);
1291
1567
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('initialData'), types.jsxExpressionContainer(types.conditionalExpression(initialDataCondition, types.optionalMemberExpression(types.identifier('props'), types.identifier(vars.propsPrefix), false, true), cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')))));
1292
1568
  }
@@ -1295,24 +1571,37 @@ function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDe
1295
1571
  // "skip-first-fetch-when-we-have-initialData" guard, which would prevent the
1296
1572
  // new sort params from reaching the fetcher. Leaving sort out of the key
1297
1573
  // lets the useMemo params identity change alone drive refetch.
1298
- dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1299
- types.templateElement({
1300
- raw: "".concat(usage.dataSourceIdentifier, "-"),
1301
- cooked: "".concat(usage.dataSourceIdentifier, "-"),
1302
- }),
1303
- types.templateElement({ raw: '-', cooked: '-' }),
1304
- types.templateElement({ raw: '', cooked: '' }),
1305
- ], [
1306
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1307
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1308
- ]))));
1574
+ //
1575
+ // ⛔ The page is left out too when the list APPENDS: remounting on every
1576
+ // appended page would unmount the accumulated rows and refetch from scratch.
1577
+ // The query stays in the key, so a new search still starts a clean list.
1578
+ dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(usage.infiniteScroll
1579
+ ? types.templateLiteral([
1580
+ types.templateElement({
1581
+ raw: "".concat(usage.dataSourceIdentifier, "-"),
1582
+ cooked: "".concat(usage.dataSourceIdentifier, "-"),
1583
+ }),
1584
+ types.templateElement({ raw: '', cooked: '' }),
1585
+ ], [
1586
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1587
+ ])
1588
+ : types.templateLiteral([
1589
+ types.templateElement({
1590
+ raw: "".concat(usage.dataSourceIdentifier, "-"),
1591
+ cooked: "".concat(usage.dataSourceIdentifier, "-"),
1592
+ }),
1593
+ types.templateElement({ raw: '-', cooked: '-' }),
1594
+ types.templateElement({ raw: '', cooked: '' }),
1595
+ ], [
1596
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1597
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1598
+ ]))));
1309
1599
  // Add fetchData
1310
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined));
1600
+ dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage));
1311
1601
  // Add persistDataDuringLoading
1312
1602
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.booleanLiteral(true))));
1313
1603
  }
1314
1604
  function updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDeclarations) {
1315
- var _a;
1316
1605
  var attrs = dp.openingElement.attributes;
1317
1606
  dp.openingElement.attributes = attrs.filter(function (attr) {
1318
1607
  var _a;
@@ -1339,20 +1628,26 @@ function updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDec
1339
1628
  // Add params
1340
1629
  var cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations);
1341
1630
  // Add initialData. See paginated+search updater for why we skip prefetch
1342
- // reuse when a dynamic state-bound sort is active.
1343
- if (!usage.dynamicSort) {
1631
+ // reuse when a dynamic state-bound sort is active, and why an appending list
1632
+ // skips it too.
1633
+ if (!usage.dynamicSort && !usage.infiniteScroll) {
1344
1634
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('initialData'), types.jsxExpressionContainer(types.conditionalExpression(wrapInitialDataWithUrlFilterGuard(types.binaryExpression('===', types.identifier(vars.pageStateVar), types.numericLiteral(1)), usage), types.optionalMemberExpression(types.identifier('props'), types.identifier(vars.propsPrefix), false, true), cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')))));
1345
1635
  }
1346
- // Add key — sort is intentionally NOT included; see paginated+search updater.
1347
- dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1348
- types.templateElement({
1349
- raw: "".concat(usage.dataSourceIdentifier, "-page-"),
1350
- cooked: "".concat(usage.dataSourceIdentifier, "-page-"),
1351
- }),
1352
- types.templateElement({ raw: '', cooked: '' }),
1353
- ], [types.identifier(vars.pageStateVar)]))));
1636
+ // Add key — sort is intentionally NOT included; see paginated+search updater,
1637
+ // which also explains why an appending list keys on nothing at all (there is
1638
+ // no query here to vary it by; a filter change restarts the accumulator
1639
+ // through its signature instead).
1640
+ if (!usage.infiniteScroll) {
1641
+ dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1642
+ types.templateElement({
1643
+ raw: "".concat(usage.dataSourceIdentifier, "-page-"),
1644
+ cooked: "".concat(usage.dataSourceIdentifier, "-page-"),
1645
+ }),
1646
+ types.templateElement({ raw: '', cooked: '' }),
1647
+ ], [types.identifier(vars.pageStateVar)]))));
1648
+ }
1354
1649
  // Add fetchData
1355
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined));
1650
+ dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage));
1356
1651
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.booleanLiteral(true))));
1357
1652
  }
1358
1653
  function updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclarations) {
@@ -1485,7 +1780,10 @@ function stabilizeDataProviderWithoutRepeater(dp) {
1485
1780
  // Replace the params attribute
1486
1781
  attrs[paramsAttrIndex] = types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(memoizedParams));
1487
1782
  }
1488
- function createFetchDataAttribute(fileName, cache) {
1783
+ function createFetchDataAttribute(fileName, cache,
1784
+ // When set, the resolved value is every page fetched so far rather than just
1785
+ // the page that was requested — see `infinite-scroll.ts`.
1786
+ accumulate) {
1489
1787
  var fetchCall = types.callExpression(types.identifier('fetch'), [
1490
1788
  types.templateLiteral([
1491
1789
  types.templateElement({
@@ -1506,7 +1804,16 @@ function createFetchDataAttribute(fileName, cache) {
1506
1804
  // The uncached tail is left exactly as it has always been emitted, so a
1507
1805
  // project that does not cache produces byte-identical output.
1508
1806
  var uncachedTail = types.arrowFunctionExpression([types.identifier('response')], types.optionalMemberExpression(types.identifier('response'), types.identifier('data'), false, true));
1509
- var networkChain = types.callExpression(types.memberExpression(parsedJson, types.identifier('then')), [cache ? (0, ast_1.buildCacheStoreThen)(cache) : uncachedTail]);
1807
+ // ⛔ Accumulation and the client cache are mutually exclusive: a cache hit
1808
+ // short-circuits the fetch and hands back one page's rows, which would drop
1809
+ // every page already accumulated. The caller suppresses the client cache for
1810
+ // an appending list, so this branch never sees both.
1811
+ var responseTail = accumulate
1812
+ ? (0, infinite_scroll_1.buildAccumulatingResponseHandler)((0, infinite_scroll_1.getInfiniteScrollVars)(accumulate.usage.index), accumulate.usage.perPage)
1813
+ : cache
1814
+ ? (0, ast_1.buildCacheStoreThen)(cache)
1815
+ : uncachedTail;
1816
+ var networkChain = types.callExpression(types.memberExpression(parsedJson, types.identifier('then')), [responseTail]);
1510
1817
  var arrow = cache
1511
1818
  ? types.arrowFunctionExpression([types.identifier('params')], (0, ast_1.buildCachedFetchBody)(networkChain, cache))
1512
1819
  : types.arrowFunctionExpression([types.identifier('params')], networkChain);
@@ -1522,45 +1829,91 @@ function wireSearchInput(inputNode, vars) {
1522
1829
  // Add value
1523
1830
  inputNode.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('value'), types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))));
1524
1831
  }
1832
+ /**
1833
+ * The `fetchData` attribute for one paginated usage.
1834
+ *
1835
+ * ⛔ An appending list gets no CLIENT cache: a cache hit resolves with one
1836
+ * page's rows without going through the accumulator, which would silently
1837
+ * discard every page already loaded. The SERVER cache is untouched — it sits
1838
+ * behind the API route and returns the same page either way.
1839
+ */
1840
+ function buildFetchDataForUsage(fileName, usage) {
1841
+ var _a;
1842
+ if (usage.infiniteScroll) {
1843
+ return createFetchDataAttribute(fileName, undefined, { usage: usage });
1844
+ }
1845
+ return createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined);
1846
+ }
1847
+ /**
1848
+ * Wires the authored Load More button of an appending list.
1849
+ *
1850
+ * Disabled — not hidden — when there is nothing left to load, so the author's
1851
+ * styling of the finished state is theirs to decide, and so the control does
1852
+ * not vanish from under the visitor's cursor mid-click.
1853
+ */
1854
+ function wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState) {
1855
+ if (!usage.infiniteScrollLoadMore) {
1856
+ return;
1857
+ }
1858
+ var button = (0, pagination_controls_1.findPaginationControl)(paginationNode, 'load-more');
1859
+ if (!button) {
1860
+ return;
1861
+ }
1862
+ var infiniteVars = (0, infinite_scroll_1.getInfiniteScrollVars)(usage.index);
1863
+ (0, pagination_controls_1.convertControlToButton)(button);
1864
+ (0, pagination_controls_1.setJSXExpressionAttribute)(button, 'onClick', types.arrowFunctionExpression([],
1865
+ // The advance statement is an expression statement; the handler needs the
1866
+ // expression itself.
1867
+ buildAdvancePageStatement(usage, vars).expression));
1868
+ // The in-flight term is only referenced when the provider declared it —
1869
+ // a mapper with no loading branch never gets `ds_N_isFetching`.
1870
+ var notHasMore = types.unaryExpression('!', types.identifier(infiniteVars.hasMoreVar), true);
1871
+ (0, pagination_controls_1.setJSXExpressionAttribute)(button, 'disabled', hasLoadingState
1872
+ ? types.logicalExpression('||', notHasMore, types.identifier((0, loading_state_1.getLoadingStateVars)(usage.index).isFetchingVar))
1873
+ : notHasMore);
1874
+ }
1875
+ /**
1876
+ * Wires one mapper's pagination container according to the mode it is in, and
1877
+ * removes the controls belonging to the other modes.
1878
+ *
1879
+ * The builder never deletes a control when the author switches mode — doing so
1880
+ * would throw away their styling — so the container can hold a superset of what
1881
+ * any one mode needs. Pruning here is what keeps a Previous/Next list from
1882
+ * rendering a dead numbered strip, and an infinite list from rendering page
1883
+ * buttons beside its Load More.
1884
+ */
1885
+ function wirePaginationWidget(paginationNode, usage, vars, hasLoadingState) {
1886
+ if (usage.infiniteScroll) {
1887
+ // Only the Load More button survives; page controls make no sense for a
1888
+ // list the visitor never leaves.
1889
+ (0, pagination_controls_1.dropInactivePaginationControls)(paginationNode, ['load-more']);
1890
+ wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState);
1891
+ return;
1892
+ }
1893
+ var activeKinds = usage.paginationMode === 'numbered'
1894
+ ? ['first', 'previous', 'pages', 'page', 'ellipsis', 'next', 'last']
1895
+ : ['previous', 'next'];
1896
+ (0, pagination_controls_1.dropInactivePaginationControls)(paginationNode, activeKinds);
1897
+ wirePaginationButtons(paginationNode, usage, vars);
1898
+ if (usage.paginationMode === 'numbered') {
1899
+ var _a = getPageAccessors(usage, vars), currentPageExpr = _a.currentPageExpr, buildSetPageStatement = _a.buildSetPageStatement;
1900
+ var numberedVars = {
1901
+ index: usage.index,
1902
+ maxPagesVar: vars.maxPagesStateVar,
1903
+ currentPageExpr: currentPageExpr,
1904
+ buildSetPageStatement: buildSetPageStatement,
1905
+ };
1906
+ (0, numbered_pagination_1.wireNumberedPagination)(paginationNode, numberedVars);
1907
+ }
1908
+ }
1525
1909
  function wirePaginationButtons(paginationNode, usage, vars) {
1526
- // Find prev and next buttons
1527
- var findButton = function (node, direction) {
1528
- var _a, _b, _c, _d, _e;
1529
- if (!node) {
1530
- return null;
1531
- }
1532
- if (node.type === 'JSXElement') {
1533
- var classAttr = (_b = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'className'; });
1534
- var className = ((_c = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _c === void 0 ? void 0 : _c.value) || ((_e = (_d = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _d === void 0 ? void 0 : _d.expression) === null || _e === void 0 ? void 0 : _e.value) || '';
1535
- if (className.includes(direction)) {
1536
- return node;
1537
- }
1538
- }
1539
- if (node.children && Array.isArray(node.children)) {
1540
- for (var _i = 0, _f = node.children; _i < _f.length; _i++) {
1541
- var c = _f[_i];
1542
- var found = findButton(c, direction);
1543
- if (found) {
1544
- return found;
1545
- }
1546
- }
1547
- }
1548
- return null;
1549
- };
1550
- var prevButton = findButton(paginationNode, 'previous');
1551
- var nextButton = findButton(paginationNode, 'next');
1910
+ // Marker attribute first, class-name substring second — projects generated
1911
+ // before the marker existed carry only the class name.
1912
+ var prevButton = (0, pagination_controls_1.findPaginationControl)(paginationNode, 'previous', 'previous');
1913
+ var nextButton = (0, pagination_controls_1.findPaginationControl)(paginationNode, 'next', 'next');
1552
1914
  var isCombinedState = usage.category === 'paginated+search';
1553
1915
  if (prevButton) {
1554
- // Change to button element
1555
- prevButton.openingElement.name.name = 'button';
1556
- if (prevButton.closingElement) {
1557
- prevButton.closingElement.name.name = 'button';
1558
- }
1559
- // Add type="button"
1560
- var hasType = prevButton.openingElement.attributes.some(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) === 'type'; });
1561
- if (!hasType) {
1562
- prevButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button')));
1563
- }
1916
+ (0, pagination_controls_1.convertControlToButton)(prevButton);
1564
1917
  // Add onClick
1565
1918
  prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) !== 'onClick'; });
1566
1919
  if (isCombinedState) {
@@ -1589,14 +1942,7 @@ function wirePaginationButtons(paginationNode, usage, vars) {
1589
1942
  }
1590
1943
  }
1591
1944
  if (nextButton) {
1592
- nextButton.openingElement.name.name = 'button';
1593
- if (nextButton.closingElement) {
1594
- nextButton.closingElement.name.name = 'button';
1595
- }
1596
- var hasType = nextButton.openingElement.attributes.some(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) === 'type'; });
1597
- if (!hasType) {
1598
- nextButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button')));
1599
- }
1945
+ (0, pagination_controls_1.convertControlToButton)(nextButton);
1600
1946
  nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) !== 'onClick'; });
1601
1947
  if (isCombinedState) {
1602
1948
  nextButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(types.arrowFunctionExpression([], types.callExpression(types.identifier(vars.setCombinedStateVar), [