@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.
- package/__tests__/_helpers/pagination-widget-fixture.ts +230 -0
- package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
- package/__tests__/ecommerce-product-discounts.test.ts +196 -0
- package/__tests__/ecommerce-product-model.test.ts +179 -0
- package/__tests__/infinite-scroll.test.ts +143 -0
- package/__tests__/numbered-pagination.test.ts +113 -0
- package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
- package/__tests__/page-url-sync.test.ts +208 -0
- package/__tests__/product-price-sort.ts +313 -0
- package/__tests__/search-url-sync.test.ts +7 -3
- package/dist/cjs/array-mapper-pagination.d.ts +9 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
- package/dist/cjs/array-mapper-pagination.js.map +1 -1
- package/dist/cjs/count-effect.d.ts +42 -0
- package/dist/cjs/count-effect.d.ts.map +1 -0
- package/dist/cjs/count-effect.js +81 -0
- package/dist/cjs/count-effect.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +65 -0
- package/dist/cjs/infinite-scroll.d.ts.map +1 -0
- package/dist/cjs/infinite-scroll.js +190 -0
- package/dist/cjs/infinite-scroll.js.map +1 -0
- package/dist/cjs/numbered-pagination.d.ts +42 -0
- package/dist/cjs/numbered-pagination.d.ts.map +1 -0
- package/dist/cjs/numbered-pagination.js +167 -0
- package/dist/cjs/numbered-pagination.js.map +1 -0
- package/dist/cjs/page-reset.d.ts +42 -0
- package/dist/cjs/page-reset.d.ts.map +1 -0
- package/dist/cjs/page-reset.js +116 -0
- package/dist/cjs/page-reset.js.map +1 -0
- package/dist/cjs/page-url-sync.d.ts +42 -0
- package/dist/cjs/page-url-sync.d.ts.map +1 -0
- package/dist/cjs/page-url-sync.js +180 -0
- package/dist/cjs/page-url-sync.js.map +1 -0
- package/dist/cjs/pagination-controls.d.ts +55 -0
- package/dist/cjs/pagination-controls.d.ts.map +1 -0
- package/dist/cjs/pagination-controls.js +159 -0
- package/dist/cjs/pagination-controls.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +506 -160
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +116 -0
- package/dist/cjs/product-price-sort.d.ts.map +1 -0
- package/dist/cjs/product-price-sort.js +169 -0
- package/dist/cjs/product-price-sort.js.map +1 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts +12 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +2 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +2 -1
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +17 -1
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/array-mapper-pagination.d.ts +9 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
- package/dist/esm/array-mapper-pagination.js.map +1 -1
- package/dist/esm/count-effect.d.ts +42 -0
- package/dist/esm/count-effect.d.ts.map +1 -0
- package/dist/esm/count-effect.js +53 -0
- package/dist/esm/count-effect.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +2 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +3 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +65 -0
- package/dist/esm/infinite-scroll.d.ts.map +1 -0
- package/dist/esm/infinite-scroll.js +159 -0
- package/dist/esm/infinite-scroll.js.map +1 -0
- package/dist/esm/numbered-pagination.d.ts +42 -0
- package/dist/esm/numbered-pagination.d.ts.map +1 -0
- package/dist/esm/numbered-pagination.js +138 -0
- package/dist/esm/numbered-pagination.js.map +1 -0
- package/dist/esm/page-reset.d.ts +42 -0
- package/dist/esm/page-reset.d.ts.map +1 -0
- package/dist/esm/page-reset.js +87 -0
- package/dist/esm/page-reset.js.map +1 -0
- package/dist/esm/page-url-sync.d.ts +42 -0
- package/dist/esm/page-url-sync.d.ts.map +1 -0
- package/dist/esm/page-url-sync.js +152 -0
- package/dist/esm/page-url-sync.js.map +1 -0
- package/dist/esm/pagination-controls.d.ts +55 -0
- package/dist/esm/pagination-controls.d.ts.map +1 -0
- package/dist/esm/pagination-controls.js +126 -0
- package/dist/esm/pagination-controls.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +507 -161
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +116 -0
- package/dist/esm/product-price-sort.d.ts.map +1 -0
- package/dist/esm/product-price-sort.js +160 -0
- package/dist/esm/product-price-sort.js.map +1 -0
- package/dist/esm/transformations/ecommerce-product.d.ts +12 -0
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +3 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +2 -1
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +17 -1
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/array-mapper-pagination.ts +10 -0
- package/src/count-effect.ts +137 -0
- package/src/fetchers/postgresql.ts +52 -13
- package/src/fetchers/teleport.ts +53 -13
- package/src/infinite-scroll.ts +401 -0
- package/src/numbered-pagination.ts +292 -0
- package/src/page-reset.ts +162 -0
- package/src/page-url-sync.ts +241 -0
- package/src/pagination-controls.ts +176 -0
- package/src/pagination-plugin.ts +755 -346
- package/src/product-price-sort.ts +204 -0
- package/src/transformations/ecommerce-product.ts +141 -10
- package/src/transformations/index.ts +19 -1
- package/src/transformations/shared-utils.ts +14 -0
- package/dist/cjs/array-mapper-registry.d.ts +0 -85
- package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
- package/dist/cjs/array-mapper-registry.js +0 -296
- package/dist/cjs/array-mapper-registry.js.map +0 -1
- package/dist/cjs/pagination-with-count.d.ts +0 -6
- package/dist/cjs/pagination-with-count.d.ts.map +0 -1
- package/dist/cjs/pagination-with-count.js +0 -63
- package/dist/cjs/pagination-with-count.js.map +0 -1
- package/dist/esm/array-mapper-registry.d.ts +0 -85
- package/dist/esm/array-mapper-registry.d.ts.map +0 -1
- package/dist/esm/array-mapper-registry.js +0 -292
- package/dist/esm/array-mapper-registry.js.map +0 -1
- package/dist/esm/pagination-with-count.d.ts +0 -6
- package/dist/esm/pagination-with-count.d.ts.map +0 -1
- package/dist/esm/pagination-with-count.js +0 -34
- package/dist/esm/pagination-with-count.js.map +0 -1
- package/src/array-mapper-registry.ts +0 -416
- package/src/pagination-with-count.ts +0 -89
package/src/pagination-plugin.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import * as types from '@babel/types'
|
|
8
8
|
import { parseExpression } from '@babel/parser'
|
|
9
9
|
import { ASTStatementOrder, StringUtils } from '@teleporthq/teleport-shared'
|
|
10
|
-
import { ASTUtils, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
|
|
10
|
+
import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
|
|
11
11
|
import { generateSafeFileName } from './utils'
|
|
12
12
|
import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
|
|
13
13
|
import {
|
|
@@ -30,6 +30,33 @@ import {
|
|
|
30
30
|
buildCachedParamsDeclarations,
|
|
31
31
|
registerCacheClientImports,
|
|
32
32
|
} from './cache/ast'
|
|
33
|
+
import {
|
|
34
|
+
convertControlToButton,
|
|
35
|
+
dropInactivePaginationControls,
|
|
36
|
+
findPaginationControl,
|
|
37
|
+
setJSXExpressionAttribute,
|
|
38
|
+
type PaginationControlKind,
|
|
39
|
+
} from './pagination-controls'
|
|
40
|
+
import { buildCountFetchEffect } from './count-effect'
|
|
41
|
+
import {
|
|
42
|
+
buildPageTokensDeclaration,
|
|
43
|
+
wireNumberedPagination,
|
|
44
|
+
type NumberedPaginationVars,
|
|
45
|
+
} from './numbered-pagination'
|
|
46
|
+
import {
|
|
47
|
+
buildAccumulatingResponseHandler,
|
|
48
|
+
buildInfiniteScrollDeclarations,
|
|
49
|
+
buildSentinelElement,
|
|
50
|
+
buildSentinelObserverEffect,
|
|
51
|
+
getInfiniteScrollVars,
|
|
52
|
+
} from './infinite-scroll'
|
|
53
|
+
import {
|
|
54
|
+
buildFilterSortPageResetEffect,
|
|
55
|
+
buildPageResetRefDeclaration,
|
|
56
|
+
hasNoPageResetDeps,
|
|
57
|
+
type PageResetDeps,
|
|
58
|
+
} from './page-reset'
|
|
59
|
+
import { buildPageUrlRefDeclaration, buildPageUrlSyncEffect } from './page-url-sync'
|
|
33
60
|
|
|
34
61
|
// ----- searchDefaultValue support -----
|
|
35
62
|
//
|
|
@@ -202,6 +229,22 @@ interface DataSourceUsage {
|
|
|
202
229
|
// cache — in which case every emit site below must produce exactly what it
|
|
203
230
|
// produced before caching existed.
|
|
204
231
|
cache?: ResolvedDataSourceCache
|
|
232
|
+
// Which pagination controls this mapper wires. `'numbered'` additionally wires
|
|
233
|
+
// First/Last and repeats the authored page-number template button; `'buttons'`
|
|
234
|
+
// is today's Previous/Next pair. Normalised to `'buttons'` whenever
|
|
235
|
+
// `infiniteScroll` is set — an accumulating list has no pages to jump between.
|
|
236
|
+
paginationMode: 'buttons' | 'numbered'
|
|
237
|
+
// Append rows instead of replacing them page by page. Suppresses every
|
|
238
|
+
// pagination control except an authored Load More button.
|
|
239
|
+
infiniteScroll: boolean
|
|
240
|
+
// With `infiniteScroll`: append on a Load More click rather than automatically
|
|
241
|
+
// when the visitor scrolls to the end of the list.
|
|
242
|
+
infiniteScrollLoadMore: boolean
|
|
243
|
+
// URL query-param key the current page is two-way bound to (e.g. `'page'`) —
|
|
244
|
+
// the pagination counterpart of `searchUrlParamKey`, sharing its
|
|
245
|
+
// `URLSearchParamSync` builders. Page 1 is written as the ABSENCE of the key.
|
|
246
|
+
// Cleared when `infiniteScroll` is set.
|
|
247
|
+
pageUrlParamKey?: string
|
|
205
248
|
// Computed category
|
|
206
249
|
category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
|
|
207
250
|
}
|
|
@@ -562,6 +605,20 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
|
|
|
562
605
|
? content.searchUrlParamKey.trim()
|
|
563
606
|
: undefined
|
|
564
607
|
|
|
608
|
+
// An accumulating list has no discrete pages, so it can neither jump
|
|
609
|
+
// between them nor put one in the URL. Normalising here — rather than at
|
|
610
|
+
// each of the ~6 emit sites — keeps every downstream branch able to trust
|
|
611
|
+
// `paginationMode` / `pageUrlParamKey` on their own.
|
|
612
|
+
const infiniteScroll = !!content.infiniteScroll
|
|
613
|
+
const pageUrlParamKey =
|
|
614
|
+
!infiniteScroll &&
|
|
615
|
+
typeof content.pageUrlParamKey === 'string' &&
|
|
616
|
+
content.pageUrlParamKey.trim().length > 0
|
|
617
|
+
? content.pageUrlParamKey.trim()
|
|
618
|
+
: undefined
|
|
619
|
+
const paginationMode: 'buttons' | 'numbered' =
|
|
620
|
+
!infiniteScroll && content.paginationMode === 'numbered' ? 'numbered' : 'buttons'
|
|
621
|
+
|
|
565
622
|
const usage: DataSourceUsage = {
|
|
566
623
|
index: index++,
|
|
567
624
|
dataSourceIdentifier: parentDataSource.identifier,
|
|
@@ -585,6 +642,10 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
|
|
|
585
642
|
filterPropIds,
|
|
586
643
|
filterUrlSearchParamKeys,
|
|
587
644
|
cache: resolveCacheFromRepeaterContent(content.cache, parentDataSource.resourceDef),
|
|
645
|
+
paginationMode,
|
|
646
|
+
infiniteScroll,
|
|
647
|
+
infiniteScrollLoadMore: infiniteScroll && !!content.infiniteScrollLoadMore,
|
|
648
|
+
pageUrlParamKey,
|
|
588
649
|
category: 'plain',
|
|
589
650
|
}
|
|
590
651
|
|
|
@@ -698,6 +759,19 @@ function usageHasSearchUrlSync(usage: DataSourceUsage): boolean {
|
|
|
698
759
|
)
|
|
699
760
|
}
|
|
700
761
|
|
|
762
|
+
// The page ⇄ URL counterpart of `usageHasSearchUrlSync`: the usage must carry a
|
|
763
|
+
// `pageUrlParamKey` AND own a page state (only the paginated categories declare
|
|
764
|
+
// `ds_N_page` / `ds_N_state.page`). `infiniteScroll` usages are already excluded
|
|
765
|
+
// by the registry, which clears their key.
|
|
766
|
+
function usageHasPageUrlSync(usage: DataSourceUsage): boolean {
|
|
767
|
+
return (
|
|
768
|
+
!!usage.pageUrlParamKey &&
|
|
769
|
+
usage.paginated &&
|
|
770
|
+
!usage.infiniteScroll &&
|
|
771
|
+
(usage.category === 'paginated+search' || usage.category === 'paginated-only')
|
|
772
|
+
)
|
|
773
|
+
}
|
|
774
|
+
|
|
701
775
|
// Emits the two URL-sync effects for a search input bound to a URL param
|
|
702
776
|
// (`searchUrlParamKey`), reusing the shared `URLSearchParamSync` builders so the
|
|
703
777
|
// search input behaves exactly like the `selectedCategory` / `sortBy` dropdowns:
|
|
@@ -754,6 +828,250 @@ function pushSearchUrlSyncEffects(
|
|
|
754
828
|
)
|
|
755
829
|
}
|
|
756
830
|
|
|
831
|
+
// How one usage reads and writes its current page.
|
|
832
|
+
//
|
|
833
|
+
// A paginated+search list keeps the page inside a combined
|
|
834
|
+
// `{ page, debouncedQuery }` object so a search reset can replace both at once,
|
|
835
|
+
// while a paginated-only list has a plain `ds_N_page`. Every control that moves
|
|
836
|
+
// the page goes through this pair rather than re-deriving the distinction.
|
|
837
|
+
function getPageAccessors(
|
|
838
|
+
usage: DataSourceUsage,
|
|
839
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
840
|
+
): {
|
|
841
|
+
currentPageExpr: () => types.Expression
|
|
842
|
+
buildSetPageStatement: (pageExpr: types.Expression) => types.Expression
|
|
843
|
+
} {
|
|
844
|
+
const isCombinedState = usage.category === 'paginated+search'
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
currentPageExpr: () =>
|
|
848
|
+
isCombinedState
|
|
849
|
+
? types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page'))
|
|
850
|
+
: types.identifier(vars.pageStateVar),
|
|
851
|
+
// Functional updates that bail when the page is unchanged: clicking the
|
|
852
|
+
// page you are already on must not schedule a render, and with it a refetch.
|
|
853
|
+
buildSetPageStatement: (pageExpr: types.Expression) =>
|
|
854
|
+
isCombinedState
|
|
855
|
+
? types.callExpression(types.identifier(vars.setCombinedStateVar), [
|
|
856
|
+
types.arrowFunctionExpression(
|
|
857
|
+
[types.identifier('state')],
|
|
858
|
+
types.conditionalExpression(
|
|
859
|
+
types.binaryExpression(
|
|
860
|
+
'===',
|
|
861
|
+
types.memberExpression(types.identifier('state'), types.identifier('page')),
|
|
862
|
+
types.cloneNode(pageExpr, true) as types.Expression
|
|
863
|
+
),
|
|
864
|
+
types.identifier('state'),
|
|
865
|
+
types.objectExpression([
|
|
866
|
+
types.spreadElement(types.identifier('state')),
|
|
867
|
+
types.objectProperty(
|
|
868
|
+
types.identifier('page'),
|
|
869
|
+
types.cloneNode(pageExpr, true) as types.Expression
|
|
870
|
+
),
|
|
871
|
+
])
|
|
872
|
+
)
|
|
873
|
+
),
|
|
874
|
+
])
|
|
875
|
+
: types.callExpression(types.identifier(vars.setPageStateVar), [
|
|
876
|
+
types.arrowFunctionExpression(
|
|
877
|
+
[types.identifier('page')],
|
|
878
|
+
types.conditionalExpression(
|
|
879
|
+
types.binaryExpression(
|
|
880
|
+
'===',
|
|
881
|
+
types.identifier('page'),
|
|
882
|
+
types.cloneNode(pageExpr, true) as types.Expression
|
|
883
|
+
),
|
|
884
|
+
types.identifier('page'),
|
|
885
|
+
types.cloneNode(pageExpr, true) as types.Expression
|
|
886
|
+
)
|
|
887
|
+
),
|
|
888
|
+
]),
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Advance one page, unless the count already says this is the last one. Shared
|
|
894
|
+
* by the scroll sentinel and the Load More button.
|
|
895
|
+
*
|
|
896
|
+
* ⛔ The next page is derived from the updater's OWN argument, never from the
|
|
897
|
+
* page in scope. The sentinel's effect is re-armed on `hasMore` / `maxPages`,
|
|
898
|
+
* and neither changes on an append while the total is unknown — so a captured
|
|
899
|
+
* `ds_N_page` would still read 1 on the second intersection and the list would
|
|
900
|
+
* stop growing after one page, on exactly the sources that cannot be counted.
|
|
901
|
+
*/
|
|
902
|
+
function buildAdvancePageStatement(
|
|
903
|
+
usage: DataSourceUsage,
|
|
904
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
905
|
+
): types.Statement {
|
|
906
|
+
const isCombinedState = usage.category === 'paginated+search'
|
|
907
|
+
const paramName = isCombinedState ? 'state' : 'page'
|
|
908
|
+
const pageOfParam = (): types.Expression =>
|
|
909
|
+
isCombinedState
|
|
910
|
+
? types.memberExpression(types.identifier(paramName), types.identifier('page'))
|
|
911
|
+
: types.identifier(paramName)
|
|
912
|
+
|
|
913
|
+
// maxPages > 0 && <param page> >= maxPages
|
|
914
|
+
const atLastPage = types.logicalExpression(
|
|
915
|
+
'&&',
|
|
916
|
+
types.binaryExpression('>', types.identifier(vars.maxPagesStateVar), types.numericLiteral(0)),
|
|
917
|
+
types.binaryExpression('>=', pageOfParam(), types.identifier(vars.maxPagesStateVar))
|
|
918
|
+
)
|
|
919
|
+
const nextPage = types.binaryExpression('+', pageOfParam(), types.numericLiteral(1))
|
|
920
|
+
|
|
921
|
+
return types.expressionStatement(
|
|
922
|
+
types.callExpression(
|
|
923
|
+
types.identifier(isCombinedState ? vars.setCombinedStateVar : vars.setPageStateVar),
|
|
924
|
+
[
|
|
925
|
+
types.arrowFunctionExpression(
|
|
926
|
+
[types.identifier(paramName)],
|
|
927
|
+
types.conditionalExpression(
|
|
928
|
+
atLastPage,
|
|
929
|
+
types.identifier(paramName),
|
|
930
|
+
isCombinedState
|
|
931
|
+
? types.objectExpression([
|
|
932
|
+
types.spreadElement(types.identifier(paramName)),
|
|
933
|
+
types.objectProperty(types.identifier('page'), nextPage),
|
|
934
|
+
])
|
|
935
|
+
: nextPage
|
|
936
|
+
)
|
|
937
|
+
),
|
|
938
|
+
]
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// The accumulator, the end-of-data flag and (in auto mode) the sentinel ref,
|
|
944
|
+
// declared only for a mapper that appends.
|
|
945
|
+
function pushInfiniteScrollDeclarations(
|
|
946
|
+
stateDeclarations: types.Statement[],
|
|
947
|
+
effectStatements: types.Statement[],
|
|
948
|
+
usage: DataSourceUsage,
|
|
949
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
950
|
+
): void {
|
|
951
|
+
if (!usage.infiniteScroll || !usage.paginated) {
|
|
952
|
+
return
|
|
953
|
+
}
|
|
954
|
+
const infiniteVars = getInfiniteScrollVars(usage.index)
|
|
955
|
+
const { currentPageExpr } = getPageAccessors(usage, vars)
|
|
956
|
+
const usesSentinel = !usage.infiniteScrollLoadMore
|
|
957
|
+
|
|
958
|
+
stateDeclarations.push(
|
|
959
|
+
...buildInfiniteScrollDeclarations(
|
|
960
|
+
infiniteVars,
|
|
961
|
+
vars.maxPagesStateVar,
|
|
962
|
+
currentPageExpr(),
|
|
963
|
+
usesSentinel
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
if (usesSentinel) {
|
|
968
|
+
effectStatements.push(
|
|
969
|
+
buildSentinelObserverEffect(infiniteVars, vars.maxPagesStateVar, () =>
|
|
970
|
+
buildAdvancePageStatement(usage, vars)
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// The `useMemo` holding the visible page numbers, declared only for a mapper in
|
|
977
|
+
// numbered mode.
|
|
978
|
+
function pushPageTokensDeclaration(
|
|
979
|
+
stateDeclarations: types.Statement[],
|
|
980
|
+
usage: DataSourceUsage,
|
|
981
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
982
|
+
): void {
|
|
983
|
+
if (usage.paginationMode !== 'numbered' || !usage.paginated) {
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
const { currentPageExpr } = getPageAccessors(usage, vars)
|
|
987
|
+
stateDeclarations.push(
|
|
988
|
+
buildPageTokensDeclaration(usage.index, vars.maxPagesStateVar, currentPageExpr())
|
|
989
|
+
)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Emits the page ⇄ `?<key>=` sync for a mapper bound to `pageUrlParamKey` — the
|
|
993
|
+
// pagination counterpart of `pushSearchUrlSyncEffects`, but a SINGLE effect
|
|
994
|
+
// that owns both directions. `page-url-sync.ts` explains at length why the
|
|
995
|
+
// usual read-back / write-back pair cannot be used here.
|
|
996
|
+
//
|
|
997
|
+
// ⛔ MUST be pushed AFTER the filter/sort reset effect. Both can fire in one
|
|
998
|
+
// flush on a navigation that changes filter and page together; effects run in
|
|
999
|
+
// emission order, so this one running LAST is what makes the URL win when it
|
|
1000
|
+
// carries a page, while a plain user filter change (whose URL has not moved
|
|
1001
|
+
// yet) is left with the reset's page 1 and writes that back.
|
|
1002
|
+
function pushPageUrlSyncEffects(
|
|
1003
|
+
stateDeclarations: types.Statement[],
|
|
1004
|
+
effectStatements: types.Statement[],
|
|
1005
|
+
usage: DataSourceUsage,
|
|
1006
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
1007
|
+
): void {
|
|
1008
|
+
if (!usageHasPageUrlSync(usage)) {
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
const refVar = `ds_${usage.index}_pageUrlRef`
|
|
1012
|
+
const { currentPageExpr, buildSetPageStatement } = getPageAccessors(usage, vars)
|
|
1013
|
+
|
|
1014
|
+
stateDeclarations.push(buildPageUrlRefDeclaration(refVar))
|
|
1015
|
+
effectStatements.push(
|
|
1016
|
+
buildPageUrlSyncEffect({
|
|
1017
|
+
paramKey: usage.pageUrlParamKey as string,
|
|
1018
|
+
refVar,
|
|
1019
|
+
currentPageExpr,
|
|
1020
|
+
buildSetPageStatement,
|
|
1021
|
+
})
|
|
1022
|
+
)
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// The name of the ref holding this usage's last-seen filter/sort signature.
|
|
1026
|
+
function getPageResetRefVar(usage: DataSourceUsage): string {
|
|
1027
|
+
return `ds_${usage.index}_filterResetRef`
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Everything that can narrow this list: filter destinations (state-, prop- and
|
|
1031
|
+
// URL-bound) plus the state a dynamic sort reads. Sort does not change WHICH
|
|
1032
|
+
// rows match, but it does change which row is on which page, so holding the
|
|
1033
|
+
// visitor on page 5 across a sort change shows them an arbitrary window of a
|
|
1034
|
+
// list they just reordered.
|
|
1035
|
+
function collectPageResetDeps(usage: DataSourceUsage): PageResetDeps {
|
|
1036
|
+
return {
|
|
1037
|
+
stateIds: [...usage.filterStateIds, ...(usage.dynamicSort?.depStateIds ?? [])],
|
|
1038
|
+
propIds: usage.filterPropIds,
|
|
1039
|
+
urlSearchParamKeys: usage.filterUrlSearchParamKeys,
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Emits the "a filter or sort changed ⇒ go back to page 1" effect for one
|
|
1044
|
+
// paginated usage. No-op for a list nothing can filter or reorder, so a mapper
|
|
1045
|
+
// without filters generates byte-for-byte what it always did.
|
|
1046
|
+
function pushFilterSortPageResetEffect(
|
|
1047
|
+
stateDeclarations: types.Statement[],
|
|
1048
|
+
effectStatements: types.Statement[],
|
|
1049
|
+
usage: DataSourceUsage,
|
|
1050
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
1051
|
+
): void {
|
|
1052
|
+
if (!usage.paginated) {
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
const deps = collectPageResetDeps(usage)
|
|
1056
|
+
if (hasNoPageResetDeps(deps)) {
|
|
1057
|
+
return
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const refVar = getPageResetRefVar(usage)
|
|
1061
|
+
stateDeclarations.push(buildPageResetRefDeclaration(refVar))
|
|
1062
|
+
|
|
1063
|
+
// The setter bails when the page is already 1 (see `getPageAccessors`), so a
|
|
1064
|
+
// visitor who was on page 1 all along and merely ticked a category does not
|
|
1065
|
+
// get an extra render — and with it an extra fetch.
|
|
1066
|
+
const { buildSetPageStatement } = getPageAccessors(usage, vars)
|
|
1067
|
+
|
|
1068
|
+
effectStatements.push(
|
|
1069
|
+
buildFilterSortPageResetEffect(refVar, deps, () =>
|
|
1070
|
+
types.expressionStatement(buildSetPageStatement(types.numericLiteral(1)))
|
|
1071
|
+
)
|
|
1072
|
+
)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
757
1075
|
// ==================== MAIN PLUGIN ====================
|
|
758
1076
|
|
|
759
1077
|
export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> = () => {
|
|
@@ -963,18 +1281,40 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
963
1281
|
types.arrowFunctionExpression(
|
|
964
1282
|
[],
|
|
965
1283
|
types.blockStatement([
|
|
1284
|
+
// ⛔ Functional, and a no-op when the query has not
|
|
1285
|
+
// actually moved. This effect fires on ANY change to
|
|
1286
|
+
// the input state — including the URL read-back
|
|
1287
|
+
// echoing back a value the visitor never typed (a
|
|
1288
|
+
// browser Forward to `?searchKeyword=x&page=3`) — and
|
|
1289
|
+
// replacing the whole object would silently drop the
|
|
1290
|
+
// page it had just adopted, as well as remount the
|
|
1291
|
+
// provider on a `key` that contains the query.
|
|
966
1292
|
types.expressionStatement(
|
|
967
1293
|
types.callExpression(types.identifier(vars.setCombinedStateVar), [
|
|
968
|
-
types.
|
|
969
|
-
types.
|
|
970
|
-
|
|
971
|
-
types.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1294
|
+
types.arrowFunctionExpression(
|
|
1295
|
+
[types.identifier('state')],
|
|
1296
|
+
types.conditionalExpression(
|
|
1297
|
+
types.binaryExpression(
|
|
1298
|
+
'===',
|
|
1299
|
+
types.memberExpression(
|
|
1300
|
+
types.identifier('state'),
|
|
1301
|
+
types.identifier('debouncedQuery')
|
|
1302
|
+
),
|
|
1303
|
+
types.identifier(vars.searchQueryVar)
|
|
1304
|
+
),
|
|
1305
|
+
types.identifier('state'),
|
|
1306
|
+
types.objectExpression([
|
|
1307
|
+
types.objectProperty(
|
|
1308
|
+
types.identifier('page'),
|
|
1309
|
+
types.numericLiteral(1)
|
|
1310
|
+
),
|
|
1311
|
+
types.objectProperty(
|
|
1312
|
+
types.identifier('debouncedQuery'),
|
|
1313
|
+
types.identifier(vars.searchQueryVar)
|
|
1314
|
+
),
|
|
1315
|
+
])
|
|
1316
|
+
)
|
|
1317
|
+
),
|
|
978
1318
|
])
|
|
979
1319
|
),
|
|
980
1320
|
])
|
|
@@ -1054,110 +1394,6 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1054
1394
|
// Add filters to count fetch params if present
|
|
1055
1395
|
appendFiltersParam(urlParams, usage.filters, buildFilterDestinationExpression)
|
|
1056
1396
|
|
|
1057
|
-
// Build the count fetch effect body
|
|
1058
|
-
const countFetchEffectBody: types.Statement[] = []
|
|
1059
|
-
|
|
1060
|
-
// Refresh the count on mount (no skip-on-mount guard). Pages seed maxPages
|
|
1061
|
-
// from the build-time `getStaticProps` count, but that snapshot goes stale:
|
|
1062
|
-
// rows added AFTER the build (e.g. the normalized product_variants table,
|
|
1063
|
-
// which is seeded/grows past one page during store generation) are not
|
|
1064
|
-
// reflected, so a frozen maxPages of 0/1 leaves "Next" permanently disabled
|
|
1065
|
-
// on data-heavy tables. Re-fetching the live count on mount corrects
|
|
1066
|
-
// maxPages so pagination tracks the real row count. The effect still runs
|
|
1067
|
-
// when the search query or a filter changes (see deps below).
|
|
1068
|
-
|
|
1069
|
-
// Add the fetch call
|
|
1070
|
-
countFetchEffectBody.push(
|
|
1071
|
-
types.expressionStatement(
|
|
1072
|
-
types.callExpression(
|
|
1073
|
-
types.memberExpression(
|
|
1074
|
-
types.callExpression(
|
|
1075
|
-
types.memberExpression(
|
|
1076
|
-
types.callExpression(types.identifier('fetch'), [
|
|
1077
|
-
types.templateLiteral(
|
|
1078
|
-
[
|
|
1079
|
-
types.templateElement({
|
|
1080
|
-
raw: `/api/${fileName}-count?`,
|
|
1081
|
-
cooked: `/api/${fileName}-count?`,
|
|
1082
|
-
}),
|
|
1083
|
-
types.templateElement({ raw: '', cooked: '' }),
|
|
1084
|
-
],
|
|
1085
|
-
[
|
|
1086
|
-
types.newExpression(types.identifier('URLSearchParams'), [
|
|
1087
|
-
types.objectExpression(urlParams),
|
|
1088
|
-
]),
|
|
1089
|
-
]
|
|
1090
|
-
),
|
|
1091
|
-
]),
|
|
1092
|
-
types.identifier('then')
|
|
1093
|
-
),
|
|
1094
|
-
[
|
|
1095
|
-
types.arrowFunctionExpression(
|
|
1096
|
-
[types.identifier('res')],
|
|
1097
|
-
types.callExpression(
|
|
1098
|
-
types.memberExpression(types.identifier('res'), types.identifier('json')),
|
|
1099
|
-
[]
|
|
1100
|
-
)
|
|
1101
|
-
),
|
|
1102
|
-
]
|
|
1103
|
-
),
|
|
1104
|
-
types.identifier('then')
|
|
1105
|
-
),
|
|
1106
|
-
[
|
|
1107
|
-
types.arrowFunctionExpression(
|
|
1108
|
-
[types.identifier('data')],
|
|
1109
|
-
types.blockStatement([
|
|
1110
|
-
types.ifStatement(
|
|
1111
|
-
types.logicalExpression(
|
|
1112
|
-
'&&',
|
|
1113
|
-
types.identifier('data'),
|
|
1114
|
-
types.binaryExpression(
|
|
1115
|
-
'in',
|
|
1116
|
-
types.stringLiteral('count'),
|
|
1117
|
-
types.identifier('data')
|
|
1118
|
-
)
|
|
1119
|
-
),
|
|
1120
|
-
types.blockStatement([
|
|
1121
|
-
types.expressionStatement(
|
|
1122
|
-
types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
|
|
1123
|
-
types.conditionalExpression(
|
|
1124
|
-
types.binaryExpression(
|
|
1125
|
-
'===',
|
|
1126
|
-
types.memberExpression(
|
|
1127
|
-
types.identifier('data'),
|
|
1128
|
-
types.identifier('count')
|
|
1129
|
-
),
|
|
1130
|
-
types.numericLiteral(0)
|
|
1131
|
-
),
|
|
1132
|
-
types.numericLiteral(0),
|
|
1133
|
-
types.callExpression(
|
|
1134
|
-
types.memberExpression(
|
|
1135
|
-
types.identifier('Math'),
|
|
1136
|
-
types.identifier('ceil')
|
|
1137
|
-
),
|
|
1138
|
-
[
|
|
1139
|
-
types.binaryExpression(
|
|
1140
|
-
'/',
|
|
1141
|
-
types.memberExpression(
|
|
1142
|
-
types.identifier('data'),
|
|
1143
|
-
types.identifier('count')
|
|
1144
|
-
),
|
|
1145
|
-
types.numericLiteral(usage.perPage)
|
|
1146
|
-
),
|
|
1147
|
-
]
|
|
1148
|
-
)
|
|
1149
|
-
),
|
|
1150
|
-
])
|
|
1151
|
-
),
|
|
1152
|
-
])
|
|
1153
|
-
),
|
|
1154
|
-
])
|
|
1155
|
-
),
|
|
1156
|
-
]
|
|
1157
|
-
)
|
|
1158
|
-
)
|
|
1159
|
-
)
|
|
1160
|
-
|
|
1161
1397
|
const countEffectDeps: types.Expression[] = [
|
|
1162
1398
|
types.memberExpression(
|
|
1163
1399
|
types.identifier(vars.combinedStateVar),
|
|
@@ -1176,16 +1412,28 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1176
1412
|
// Same goes for URL-driven filters (already documented above).
|
|
1177
1413
|
pushUrlSearchParamMemoDeps(countEffectDeps, usage)
|
|
1178
1414
|
effectStatements.push(
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1415
|
+
buildCountFetchEffect({
|
|
1416
|
+
fileName,
|
|
1417
|
+
perPage: usage.perPage,
|
|
1418
|
+
setMaxPagesVar: vars.setMaxPagesStateVar,
|
|
1419
|
+
urlParams,
|
|
1420
|
+
deps: countEffectDeps,
|
|
1421
|
+
})
|
|
1185
1422
|
)
|
|
1186
1423
|
|
|
1187
1424
|
// Two-way URL sync for the search input when bound to a URL param.
|
|
1188
1425
|
pushSearchUrlSyncEffects(effectStatements, usage, vars)
|
|
1426
|
+
|
|
1427
|
+
// Visible page numbers, when this mapper draws a numbered strip.
|
|
1428
|
+
pushPageTokensDeclaration(stateDeclarations, usage, vars)
|
|
1429
|
+
// Accumulation state, when it appends instead of paging.
|
|
1430
|
+
pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars)
|
|
1431
|
+
|
|
1432
|
+
// A filter or sort change invalidates the page the visitor is on.
|
|
1433
|
+
pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars)
|
|
1434
|
+
// Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
|
|
1435
|
+
// after the reset so a URL that names a page wins over it.
|
|
1436
|
+
pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars)
|
|
1189
1437
|
} else if (usage.category === 'paginated-only') {
|
|
1190
1438
|
// Simple page state
|
|
1191
1439
|
const maxPagesInit = isPage
|
|
@@ -1225,122 +1473,72 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1225
1473
|
])
|
|
1226
1474
|
)
|
|
1227
1475
|
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1476
|
+
// The live row count behind `ds_N_maxPages`.
|
|
1477
|
+
//
|
|
1478
|
+
// Components have never had a build-time count to fall back on, so they
|
|
1479
|
+
// have always fetched one here. Pages seed theirs from `getStaticProps`,
|
|
1480
|
+
// which only knows the STATIC filters — good enough to enable/disable a
|
|
1481
|
+
// "Next" button, but not to label a numbered strip or to know when an
|
|
1482
|
+
// infinite list has reached its end, so those two modes fetch a live
|
|
1483
|
+
// count as well.
|
|
1484
|
+
const needsLiveCount =
|
|
1485
|
+
!isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll
|
|
1486
|
+
if (needsLiveCount) {
|
|
1231
1487
|
const fileName = generateSafeFileName(
|
|
1232
1488
|
usage.resourceDefinition.dataSourceType,
|
|
1233
1489
|
usage.resourceDefinition.tableName,
|
|
1234
1490
|
usage.resourceDefinition.dataSourceId
|
|
1235
1491
|
)
|
|
1236
1492
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
types.identifier('then')
|
|
1252
|
-
),
|
|
1253
|
-
[
|
|
1254
|
-
types.arrowFunctionExpression(
|
|
1255
|
-
[types.identifier('res')],
|
|
1256
|
-
types.callExpression(
|
|
1257
|
-
types.memberExpression(
|
|
1258
|
-
types.identifier('res'),
|
|
1259
|
-
types.identifier('json')
|
|
1260
|
-
),
|
|
1261
|
-
[]
|
|
1262
|
-
)
|
|
1263
|
-
),
|
|
1264
|
-
]
|
|
1265
|
-
),
|
|
1266
|
-
types.identifier('then')
|
|
1267
|
-
),
|
|
1268
|
-
[
|
|
1269
|
-
types.arrowFunctionExpression(
|
|
1270
|
-
[types.identifier('data')],
|
|
1271
|
-
types.blockStatement([
|
|
1272
|
-
types.ifStatement(
|
|
1273
|
-
types.logicalExpression(
|
|
1274
|
-
'&&',
|
|
1275
|
-
types.identifier('data'),
|
|
1276
|
-
types.binaryExpression(
|
|
1277
|
-
'in',
|
|
1278
|
-
types.stringLiteral('count'),
|
|
1279
|
-
types.identifier('data')
|
|
1280
|
-
)
|
|
1281
|
-
),
|
|
1282
|
-
types.blockStatement([
|
|
1283
|
-
types.expressionStatement(
|
|
1284
|
-
types.callExpression(
|
|
1285
|
-
types.identifier(vars.setMaxPagesStateVar),
|
|
1286
|
-
[
|
|
1287
|
-
types.conditionalExpression(
|
|
1288
|
-
types.binaryExpression(
|
|
1289
|
-
'===',
|
|
1290
|
-
types.memberExpression(
|
|
1291
|
-
types.identifier('data'),
|
|
1292
|
-
types.identifier('count')
|
|
1293
|
-
),
|
|
1294
|
-
types.numericLiteral(0)
|
|
1295
|
-
),
|
|
1296
|
-
types.numericLiteral(0),
|
|
1297
|
-
types.callExpression(
|
|
1298
|
-
types.memberExpression(
|
|
1299
|
-
types.identifier('Math'),
|
|
1300
|
-
types.identifier('ceil')
|
|
1301
|
-
),
|
|
1302
|
-
[
|
|
1303
|
-
types.binaryExpression(
|
|
1304
|
-
'/',
|
|
1305
|
-
types.memberExpression(
|
|
1306
|
-
types.identifier('data'),
|
|
1307
|
-
types.identifier('count')
|
|
1308
|
-
),
|
|
1309
|
-
types.numericLiteral(usage.perPage)
|
|
1310
|
-
),
|
|
1311
|
-
]
|
|
1312
|
-
)
|
|
1313
|
-
),
|
|
1314
|
-
]
|
|
1315
|
-
)
|
|
1316
|
-
),
|
|
1317
|
-
])
|
|
1318
|
-
),
|
|
1319
|
-
])
|
|
1320
|
-
),
|
|
1321
|
-
]
|
|
1322
|
-
)
|
|
1323
|
-
),
|
|
1324
|
-
])
|
|
1325
|
-
),
|
|
1326
|
-
// Default to mount-only; refresh when ANY filter destination
|
|
1327
|
-
// changes — state-bound (e.g. `selectedCategory`) and
|
|
1328
|
-
// URL-driven — so the pagination control reflects the current
|
|
1329
|
-
// filtered count instead of the unfiltered mount-time total.
|
|
1330
|
-
types.arrayExpression(
|
|
1331
|
-
((): types.Expression[] => {
|
|
1332
|
-
const deps: types.Expression[] = []
|
|
1333
|
-
const depsSeen = new Set<string>()
|
|
1334
|
-
pushStateIdsAsDeps(deps, depsSeen, usage.filterStateIds)
|
|
1335
|
-
pushPropIdsAsDeps(deps, depsSeen, usage.filterPropIds)
|
|
1336
|
-
pushUrlSearchParamMemoDeps(deps, usage)
|
|
1337
|
-
return deps
|
|
1338
|
-
})()
|
|
1339
|
-
),
|
|
1340
|
-
])
|
|
1493
|
+
// ⛔ This list has no search input, so no `query` param — but it may
|
|
1494
|
+
// well have filters, and omitting them (as this call site did until
|
|
1495
|
+
// the shared builder replaced it) counts the WHOLE table: "Next"
|
|
1496
|
+
// stayed enabled far past the end of a filtered result set.
|
|
1497
|
+
const countUrlParams: types.ObjectProperty[] = []
|
|
1498
|
+
if (usage.queryColumns.length > 0) {
|
|
1499
|
+
countUrlParams.push(
|
|
1500
|
+
types.objectProperty(
|
|
1501
|
+
types.identifier('queryColumns'),
|
|
1502
|
+
types.callExpression(
|
|
1503
|
+
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
1504
|
+
[types.arrayExpression(usage.queryColumns.map((c) => types.stringLiteral(c)))]
|
|
1505
|
+
)
|
|
1506
|
+
)
|
|
1341
1507
|
)
|
|
1508
|
+
}
|
|
1509
|
+
appendFiltersParam(countUrlParams, usage.filters, buildFilterDestinationExpression)
|
|
1510
|
+
|
|
1511
|
+
// Default to mount-only; refresh when ANY filter destination
|
|
1512
|
+
// changes — state-bound (e.g. `selectedCategory`) and URL-driven —
|
|
1513
|
+
// so the pagination control reflects the current filtered count
|
|
1514
|
+
// instead of the unfiltered mount-time total.
|
|
1515
|
+
const countDeps: types.Expression[] = []
|
|
1516
|
+
const countDepsSeen = new Set<string>()
|
|
1517
|
+
pushStateIdsAsDeps(countDeps, countDepsSeen, usage.filterStateIds)
|
|
1518
|
+
pushPropIdsAsDeps(countDeps, countDepsSeen, usage.filterPropIds)
|
|
1519
|
+
pushUrlSearchParamMemoDeps(countDeps, usage)
|
|
1520
|
+
|
|
1521
|
+
effectStatements.push(
|
|
1522
|
+
buildCountFetchEffect({
|
|
1523
|
+
fileName,
|
|
1524
|
+
perPage: usage.perPage,
|
|
1525
|
+
setMaxPagesVar: vars.setMaxPagesStateVar,
|
|
1526
|
+
urlParams: countUrlParams,
|
|
1527
|
+
deps: countDeps,
|
|
1528
|
+
})
|
|
1342
1529
|
)
|
|
1343
1530
|
}
|
|
1531
|
+
|
|
1532
|
+
// Visible page numbers, when this mapper draws a numbered strip.
|
|
1533
|
+
pushPageTokensDeclaration(stateDeclarations, usage, vars)
|
|
1534
|
+
// Accumulation state, when it appends instead of paging.
|
|
1535
|
+
pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars)
|
|
1536
|
+
|
|
1537
|
+
// A filter or sort change invalidates the page the visitor is on.
|
|
1538
|
+
pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars)
|
|
1539
|
+
// Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
|
|
1540
|
+
// after the reset so a URL that names a page wins over it.
|
|
1541
|
+
pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars)
|
|
1344
1542
|
} else if (usage.category === 'search-only') {
|
|
1345
1543
|
// Search-only state
|
|
1346
1544
|
stateDeclarations.push(
|
|
@@ -1464,7 +1662,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1464
1662
|
// same predicate as the effects themselves so router is only injected when
|
|
1465
1663
|
// it is actually used.
|
|
1466
1664
|
const needsUseRouter = registry.usages.some(
|
|
1467
|
-
(u) => hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u)
|
|
1665
|
+
(u) => hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u) || usageHasPageUrlSync(u)
|
|
1468
1666
|
)
|
|
1469
1667
|
if (needsUseRouter) {
|
|
1470
1668
|
if (!dependencies.useRouter) {
|
|
@@ -1502,6 +1700,14 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1502
1700
|
}
|
|
1503
1701
|
}
|
|
1504
1702
|
|
|
1703
|
+
// Both the search write-back and the page ⇄ URL effect write the URL through
|
|
1704
|
+
// the shared `URLQueryWriter`, so the ref + helper they close over must be in
|
|
1705
|
+
// the body. Idempotent with `createNextUrlSearchParamsPlugin`, which needs the
|
|
1706
|
+
// same pair for the category/sort dropdowns and may have run first.
|
|
1707
|
+
if (registry.usages.some((u) => usageHasSearchUrlSync(u) || usageHasPageUrlSync(u))) {
|
|
1708
|
+
URLQueryWriter.ensureQuerySyncDeclarations(blockStatement.body)
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1505
1711
|
// Insert effects before return statement
|
|
1506
1712
|
const returnIndex = blockStatement.body.findIndex((s: any) => s.type === 'ReturnStatement')
|
|
1507
1713
|
const insertIndex = returnIndex !== -1 ? returnIndex : blockStatement.body.length
|
|
@@ -1525,6 +1731,11 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1525
1731
|
// provider can use one depends on its JSX (it needs a `renderLoading` slot),
|
|
1526
1732
|
// which is only known once the category updaters above have run.
|
|
1527
1733
|
const loadingStateDeclarations: types.Statement[] = []
|
|
1734
|
+
// Usage indexes whose provider actually got the refetch loading state.
|
|
1735
|
+
const usagesWithLoadingState = new Set<number>()
|
|
1736
|
+
// Which mapper each `DataProvider` JSX element belongs to, so the pagination
|
|
1737
|
+
// widgets can be matched to their owner rather than counted off in order.
|
|
1738
|
+
const usageByProviderNode = new Map<any, DataSourceUsage>()
|
|
1528
1739
|
const cacheDeclarations: types.Statement[] = []
|
|
1529
1740
|
|
|
1530
1741
|
dataProvidersWithRepeaters.forEach((dp) => {
|
|
@@ -1547,6 +1758,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1547
1758
|
|
|
1548
1759
|
const usage = usages[currentIndex]
|
|
1549
1760
|
usageIndexByDataSourceId.set(dataSourceIdentifier, currentIndex + 1)
|
|
1761
|
+
usageByProviderNode.set(dp, usage)
|
|
1550
1762
|
|
|
1551
1763
|
const vars = getStateVarsForUsage(usage)
|
|
1552
1764
|
const fileName = generateSafeFileName(
|
|
@@ -1573,6 +1785,20 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1573
1785
|
const loadingVars = getLoadingStateVars(usage.index)
|
|
1574
1786
|
if (applyLoadingStateToDataProvider(dp, loadingVars)) {
|
|
1575
1787
|
loadingStateDeclarations.push(...buildLoadingStateDeclarations(loadingVars))
|
|
1788
|
+
// Recorded because the loading state is only declared for a provider
|
|
1789
|
+
// that HAS a loading slot; a Load More button wired later may only
|
|
1790
|
+
// reference `ds_N_isFetching` if it actually exists.
|
|
1791
|
+
usagesWithLoadingState.add(usage.index)
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// An auto-loading list needs something at the end of the rows for its
|
|
1795
|
+
// IntersectionObserver to watch.
|
|
1796
|
+
if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
|
|
1797
|
+
insertSentinelAfterProvider(
|
|
1798
|
+
blockStatement,
|
|
1799
|
+
dp,
|
|
1800
|
+
buildSentinelElement(getInfiniteScrollVars(usage.index))
|
|
1801
|
+
)
|
|
1576
1802
|
}
|
|
1577
1803
|
|
|
1578
1804
|
// Create API route for all categories (including 'plain' for components)
|
|
@@ -1658,23 +1884,30 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1658
1884
|
wireSearchInput(input.node, vars)
|
|
1659
1885
|
})
|
|
1660
1886
|
|
|
1661
|
-
// STEP 5: Wire pagination
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
//
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
//
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1887
|
+
// STEP 5: Wire pagination widgets
|
|
1888
|
+
//
|
|
1889
|
+
// Each widget belongs to the mapper whose `DataProvider` it follows in the
|
|
1890
|
+
// document — the builder emits it as a sibling of (or inside) that provider.
|
|
1891
|
+
//
|
|
1892
|
+
// ⛔ Not a positional zip of "widgets" against "paginated usages": the two
|
|
1893
|
+
// lists no longer have to be the same length, because an infinite-scroll
|
|
1894
|
+
// mapper renders no widget. Counting one for it would shift every later
|
|
1895
|
+
// mapper onto the wrong usage and wire one list's buttons to another list's
|
|
1896
|
+
// state. Anchoring on the provider cannot drift for the same reason the
|
|
1897
|
+
// provider loop above cannot.
|
|
1898
|
+
findPaginationNodesWithOwners(blockStatement, usageByProviderNode).forEach(
|
|
1899
|
+
({ node, usage }) => {
|
|
1900
|
+
if (!usage) {
|
|
1901
|
+
return
|
|
1902
|
+
}
|
|
1903
|
+
wirePaginationWidget(
|
|
1904
|
+
node,
|
|
1905
|
+
usage,
|
|
1906
|
+
getStateVarsForUsage(usage),
|
|
1907
|
+
usagesWithLoadingState.has(usage.index)
|
|
1908
|
+
)
|
|
1672
1909
|
}
|
|
1673
|
-
|
|
1674
|
-
const usage = paginatedUsages[idx]
|
|
1675
|
-
const vars = getStateVarsForUsage(usage)
|
|
1676
|
-
wirePaginationButtons(paginationNode.node, usage, vars)
|
|
1677
|
-
})
|
|
1910
|
+
)
|
|
1678
1911
|
|
|
1679
1912
|
// STEP 6: Update getStaticProps if this is a page
|
|
1680
1913
|
if (isPage) {
|
|
@@ -1689,6 +1922,63 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1689
1922
|
|
|
1690
1923
|
// ==================== HELPER FUNCTIONS ====================
|
|
1691
1924
|
|
|
1925
|
+
/**
|
|
1926
|
+
* Inserts the scroll sentinel immediately after a `DataProvider`, as its next
|
|
1927
|
+
* sibling, so it sits below the rendered rows.
|
|
1928
|
+
*
|
|
1929
|
+
* It has to be a sibling: the provider's own child is a render-prop function,
|
|
1930
|
+
* and anything placed inside it would be re-created on every fetch — the
|
|
1931
|
+
* observer would then be watching an element that is no longer in the document.
|
|
1932
|
+
*
|
|
1933
|
+
* Returns false when the provider has no element parent to splice into (a
|
|
1934
|
+
* provider returned directly from the component), leaving the list on its
|
|
1935
|
+
* count-driven `hasMore` rather than injecting a stray node.
|
|
1936
|
+
*/
|
|
1937
|
+
function insertSentinelAfterProvider(
|
|
1938
|
+
blockStatement: types.BlockStatement,
|
|
1939
|
+
provider: any,
|
|
1940
|
+
sentinel: types.JSXElement
|
|
1941
|
+
): boolean {
|
|
1942
|
+
let inserted = false
|
|
1943
|
+
|
|
1944
|
+
const traverse = (node: any): void => {
|
|
1945
|
+
if (!node || inserted) {
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
if (Array.isArray(node.children)) {
|
|
1949
|
+
const at = node.children.indexOf(provider)
|
|
1950
|
+
if (at !== -1) {
|
|
1951
|
+
node.children.splice(at + 1, 0, sentinel)
|
|
1952
|
+
inserted = true
|
|
1953
|
+
return
|
|
1954
|
+
}
|
|
1955
|
+
node.children.forEach(traverse)
|
|
1956
|
+
}
|
|
1957
|
+
if (node.body) {
|
|
1958
|
+
if (Array.isArray(node.body)) {
|
|
1959
|
+
node.body.forEach(traverse)
|
|
1960
|
+
} else {
|
|
1961
|
+
traverse(node.body)
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (node.consequent) {
|
|
1965
|
+
traverse(node.consequent)
|
|
1966
|
+
}
|
|
1967
|
+
if (node.alternate) {
|
|
1968
|
+
traverse(node.alternate)
|
|
1969
|
+
}
|
|
1970
|
+
if (node.expression) {
|
|
1971
|
+
traverse(node.expression)
|
|
1972
|
+
}
|
|
1973
|
+
if (node.argument) {
|
|
1974
|
+
traverse(node.argument)
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
traverse(blockStatement)
|
|
1979
|
+
return inserted
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1692
1982
|
function findAllDataProvidersInJSX(blockStatement: types.BlockStatement): any[] {
|
|
1693
1983
|
const results: any[] = []
|
|
1694
1984
|
|
|
@@ -1835,10 +2125,21 @@ function findAllSearchInputsInJSX(
|
|
|
1835
2125
|
return results
|
|
1836
2126
|
}
|
|
1837
2127
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2128
|
+
/**
|
|
2129
|
+
* Every pagination widget in the JSX, paired with the mapper it belongs to.
|
|
2130
|
+
*
|
|
2131
|
+
* Ownership follows document order: a widget belongs to the most recent
|
|
2132
|
+
* `DataProvider` seen before it, which is exactly how the builder emits it —
|
|
2133
|
+
* as a sibling that follows its provider, or nested inside that provider's
|
|
2134
|
+
* subtree. A widget that precedes every provider (nothing sensible to wire it
|
|
2135
|
+
* to) comes back with no owner and is left alone.
|
|
2136
|
+
*/
|
|
2137
|
+
function findPaginationNodesWithOwners(
|
|
2138
|
+
blockStatement: types.BlockStatement,
|
|
2139
|
+
usageByProviderNode: Map<any, DataSourceUsage>
|
|
2140
|
+
): Array<{ node: any; usage?: DataSourceUsage }> {
|
|
2141
|
+
const results: Array<{ node: any; usage?: DataSourceUsage }> = []
|
|
2142
|
+
let currentUsage: DataSourceUsage | undefined
|
|
1842
2143
|
|
|
1843
2144
|
const traverse = (node: any): void => {
|
|
1844
2145
|
if (!node) {
|
|
@@ -1846,12 +2147,16 @@ function findAllPaginationNodesInJSX(
|
|
|
1846
2147
|
}
|
|
1847
2148
|
|
|
1848
2149
|
if (node.type === 'JSXElement') {
|
|
2150
|
+
const owner = usageByProviderNode.get(node)
|
|
2151
|
+
if (owner) {
|
|
2152
|
+
currentUsage = owner
|
|
2153
|
+
}
|
|
1849
2154
|
const classAttr = node.openingElement?.attributes?.find(
|
|
1850
2155
|
(attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'className'
|
|
1851
2156
|
)
|
|
1852
2157
|
const className = classAttr?.value?.value || classAttr?.value?.expression?.value || ''
|
|
1853
2158
|
if (className.includes('cms-pagination-node')) {
|
|
1854
|
-
results.push({ node,
|
|
2159
|
+
results.push({ node, usage: currentUsage })
|
|
1855
2160
|
}
|
|
1856
2161
|
}
|
|
1857
2162
|
|
|
@@ -2000,7 +2305,12 @@ function updateDataProviderForPaginatedSearch(
|
|
|
2000
2305
|
// the first client fetch (its internal guard only skips when initialData is
|
|
2001
2306
|
// defined on mount). Without that skip, toggling sort correctly triggers a
|
|
2002
2307
|
// refetch via the useMemo params dependency chain.
|
|
2003
|
-
|
|
2308
|
+
//
|
|
2309
|
+
// An appending list skips it for a second reason: `initialData` makes
|
|
2310
|
+
// DataProvider skip its first fetch, so page 1 would render from the prefetch
|
|
2311
|
+
// and never enter the accumulator — appending page 2 would then REPLACE the
|
|
2312
|
+
// visible rows with page 2 alone.
|
|
2313
|
+
if (!usage.dynamicSort && !usage.infiniteScroll) {
|
|
2004
2314
|
const initialDataCondition = wrapInitialDataWithUrlFilterGuard(
|
|
2005
2315
|
types.logicalExpression(
|
|
2006
2316
|
'&&',
|
|
@@ -2044,38 +2354,56 @@ function updateDataProviderForPaginatedSearch(
|
|
|
2044
2354
|
// "skip-first-fetch-when-we-have-initialData" guard, which would prevent the
|
|
2045
2355
|
// new sort params from reaching the fetcher. Leaving sort out of the key
|
|
2046
2356
|
// lets the useMemo params identity change alone drive refetch.
|
|
2357
|
+
//
|
|
2358
|
+
// ⛔ The page is left out too when the list APPENDS: remounting on every
|
|
2359
|
+
// appended page would unmount the accumulated rows and refetch from scratch.
|
|
2360
|
+
// The query stays in the key, so a new search still starts a clean list.
|
|
2047
2361
|
dp.openingElement.attributes.push(
|
|
2048
2362
|
types.jsxAttribute(
|
|
2049
2363
|
types.jsxIdentifier('key'),
|
|
2050
2364
|
types.jsxExpressionContainer(
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2365
|
+
usage.infiniteScroll
|
|
2366
|
+
? types.templateLiteral(
|
|
2367
|
+
[
|
|
2368
|
+
types.templateElement({
|
|
2369
|
+
raw: `${usage.dataSourceIdentifier}-`,
|
|
2370
|
+
cooked: `${usage.dataSourceIdentifier}-`,
|
|
2371
|
+
}),
|
|
2372
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
2373
|
+
],
|
|
2374
|
+
[
|
|
2375
|
+
types.memberExpression(
|
|
2376
|
+
types.identifier(vars.combinedStateVar),
|
|
2377
|
+
types.identifier('debouncedQuery')
|
|
2378
|
+
),
|
|
2379
|
+
]
|
|
2380
|
+
)
|
|
2381
|
+
: types.templateLiteral(
|
|
2382
|
+
[
|
|
2383
|
+
types.templateElement({
|
|
2384
|
+
raw: `${usage.dataSourceIdentifier}-`,
|
|
2385
|
+
cooked: `${usage.dataSourceIdentifier}-`,
|
|
2386
|
+
}),
|
|
2387
|
+
types.templateElement({ raw: '-', cooked: '-' }),
|
|
2388
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
2389
|
+
],
|
|
2390
|
+
[
|
|
2391
|
+
types.memberExpression(
|
|
2392
|
+
types.identifier(vars.combinedStateVar),
|
|
2393
|
+
types.identifier('page')
|
|
2394
|
+
),
|
|
2395
|
+
types.memberExpression(
|
|
2396
|
+
types.identifier(vars.combinedStateVar),
|
|
2397
|
+
types.identifier('debouncedQuery')
|
|
2398
|
+
),
|
|
2399
|
+
]
|
|
2400
|
+
)
|
|
2071
2401
|
)
|
|
2072
2402
|
)
|
|
2073
2403
|
)
|
|
2074
2404
|
|
|
2075
2405
|
// Add fetchData
|
|
2076
|
-
dp.openingElement.attributes.push(
|
|
2077
|
-
createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
|
|
2078
|
-
)
|
|
2406
|
+
dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage))
|
|
2079
2407
|
|
|
2080
2408
|
// Add persistDataDuringLoading
|
|
2081
2409
|
dp.openingElement.attributes.push(
|
|
@@ -2128,8 +2456,9 @@ function updateDataProviderForPaginationOnly(
|
|
|
2128
2456
|
const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
|
|
2129
2457
|
|
|
2130
2458
|
// Add initialData. See paginated+search updater for why we skip prefetch
|
|
2131
|
-
// reuse when a dynamic state-bound sort is active
|
|
2132
|
-
|
|
2459
|
+
// reuse when a dynamic state-bound sort is active, and why an appending list
|
|
2460
|
+
// skips it too.
|
|
2461
|
+
if (!usage.dynamicSort && !usage.infiniteScroll) {
|
|
2133
2462
|
dp.openingElement.attributes.push(
|
|
2134
2463
|
types.jsxAttribute(
|
|
2135
2464
|
types.jsxIdentifier('initialData'),
|
|
@@ -2156,29 +2485,32 @@ function updateDataProviderForPaginationOnly(
|
|
|
2156
2485
|
)
|
|
2157
2486
|
}
|
|
2158
2487
|
|
|
2159
|
-
// Add key — sort is intentionally NOT included; see paginated+search updater
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2488
|
+
// Add key — sort is intentionally NOT included; see paginated+search updater,
|
|
2489
|
+
// which also explains why an appending list keys on nothing at all (there is
|
|
2490
|
+
// no query here to vary it by; a filter change restarts the accumulator
|
|
2491
|
+
// through its signature instead).
|
|
2492
|
+
if (!usage.infiniteScroll) {
|
|
2493
|
+
dp.openingElement.attributes.push(
|
|
2494
|
+
types.jsxAttribute(
|
|
2495
|
+
types.jsxIdentifier('key'),
|
|
2496
|
+
types.jsxExpressionContainer(
|
|
2497
|
+
types.templateLiteral(
|
|
2498
|
+
[
|
|
2499
|
+
types.templateElement({
|
|
2500
|
+
raw: `${usage.dataSourceIdentifier}-page-`,
|
|
2501
|
+
cooked: `${usage.dataSourceIdentifier}-page-`,
|
|
2502
|
+
}),
|
|
2503
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
2504
|
+
],
|
|
2505
|
+
[types.identifier(vars.pageStateVar)]
|
|
2506
|
+
)
|
|
2173
2507
|
)
|
|
2174
2508
|
)
|
|
2175
2509
|
)
|
|
2176
|
-
|
|
2510
|
+
}
|
|
2177
2511
|
|
|
2178
2512
|
// Add fetchData
|
|
2179
|
-
dp.openingElement.attributes.push(
|
|
2180
|
-
createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
|
|
2181
|
-
)
|
|
2513
|
+
dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage))
|
|
2182
2514
|
|
|
2183
2515
|
dp.openingElement.attributes.push(
|
|
2184
2516
|
types.jsxAttribute(
|
|
@@ -2430,7 +2762,10 @@ function stabilizeDataProviderWithoutRepeater(dp: any): void {
|
|
|
2430
2762
|
|
|
2431
2763
|
function createFetchDataAttribute(
|
|
2432
2764
|
fileName: string,
|
|
2433
|
-
cache?: ResolvedDataSourceCache
|
|
2765
|
+
cache?: ResolvedDataSourceCache,
|
|
2766
|
+
// When set, the resolved value is every page fetched so far rather than just
|
|
2767
|
+
// the page that was requested — see `infinite-scroll.ts`.
|
|
2768
|
+
accumulate?: { usage: DataSourceUsage }
|
|
2434
2769
|
): types.JSXAttribute {
|
|
2435
2770
|
const fetchCall = types.callExpression(types.identifier('fetch'), [
|
|
2436
2771
|
types.templateLiteral(
|
|
@@ -2481,9 +2816,22 @@ function createFetchDataAttribute(
|
|
|
2481
2816
|
)
|
|
2482
2817
|
)
|
|
2483
2818
|
|
|
2819
|
+
// ⛔ Accumulation and the client cache are mutually exclusive: a cache hit
|
|
2820
|
+
// short-circuits the fetch and hands back one page's rows, which would drop
|
|
2821
|
+
// every page already accumulated. The caller suppresses the client cache for
|
|
2822
|
+
// an appending list, so this branch never sees both.
|
|
2823
|
+
const responseTail = accumulate
|
|
2824
|
+
? buildAccumulatingResponseHandler(
|
|
2825
|
+
getInfiniteScrollVars(accumulate.usage.index),
|
|
2826
|
+
accumulate.usage.perPage
|
|
2827
|
+
)
|
|
2828
|
+
: cache
|
|
2829
|
+
? buildCacheStoreThen(cache)
|
|
2830
|
+
: uncachedTail
|
|
2831
|
+
|
|
2484
2832
|
const networkChain = types.callExpression(
|
|
2485
2833
|
types.memberExpression(parsedJson, types.identifier('then')),
|
|
2486
|
-
[
|
|
2834
|
+
[responseTail]
|
|
2487
2835
|
)
|
|
2488
2836
|
|
|
2489
2837
|
const arrow = cache
|
|
@@ -2534,57 +2882,128 @@ function wireSearchInput(inputNode: any, vars: ReturnType<typeof getStateVarsFor
|
|
|
2534
2882
|
)
|
|
2535
2883
|
}
|
|
2536
2884
|
|
|
2537
|
-
|
|
2885
|
+
/**
|
|
2886
|
+
* The `fetchData` attribute for one paginated usage.
|
|
2887
|
+
*
|
|
2888
|
+
* ⛔ An appending list gets no CLIENT cache: a cache hit resolves with one
|
|
2889
|
+
* page's rows without going through the accumulator, which would silently
|
|
2890
|
+
* discard every page already loaded. The SERVER cache is untouched — it sits
|
|
2891
|
+
* behind the API route and returns the same page either way.
|
|
2892
|
+
*/
|
|
2893
|
+
function buildFetchDataForUsage(fileName: string, usage: DataSourceUsage): types.JSXAttribute {
|
|
2894
|
+
if (usage.infiniteScroll) {
|
|
2895
|
+
return createFetchDataAttribute(fileName, undefined, { usage })
|
|
2896
|
+
}
|
|
2897
|
+
return createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* Wires the authored Load More button of an appending list.
|
|
2902
|
+
*
|
|
2903
|
+
* Disabled — not hidden — when there is nothing left to load, so the author's
|
|
2904
|
+
* styling of the finished state is theirs to decide, and so the control does
|
|
2905
|
+
* not vanish from under the visitor's cursor mid-click.
|
|
2906
|
+
*/
|
|
2907
|
+
function wireLoadMoreButton(
|
|
2538
2908
|
paginationNode: any,
|
|
2539
2909
|
usage: DataSourceUsage,
|
|
2540
|
-
vars: ReturnType<typeof getStateVarsForUsage
|
|
2910
|
+
vars: ReturnType<typeof getStateVarsForUsage>,
|
|
2911
|
+
hasLoadingState: boolean
|
|
2541
2912
|
): void {
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2913
|
+
if (!usage.infiniteScrollLoadMore) {
|
|
2914
|
+
return
|
|
2915
|
+
}
|
|
2916
|
+
const button = findPaginationControl(paginationNode, 'load-more')
|
|
2917
|
+
if (!button) {
|
|
2918
|
+
return
|
|
2919
|
+
}
|
|
2920
|
+
const infiniteVars = getInfiniteScrollVars(usage.index)
|
|
2921
|
+
|
|
2922
|
+
convertControlToButton(button)
|
|
2923
|
+
setJSXExpressionAttribute(
|
|
2924
|
+
button,
|
|
2925
|
+
'onClick',
|
|
2926
|
+
types.arrowFunctionExpression(
|
|
2927
|
+
[],
|
|
2928
|
+
// The advance statement is an expression statement; the handler needs the
|
|
2929
|
+
// expression itself.
|
|
2930
|
+
(buildAdvancePageStatement(usage, vars) as types.ExpressionStatement).expression
|
|
2931
|
+
)
|
|
2932
|
+
)
|
|
2933
|
+
// The in-flight term is only referenced when the provider declared it —
|
|
2934
|
+
// a mapper with no loading branch never gets `ds_N_isFetching`.
|
|
2935
|
+
const notHasMore = types.unaryExpression('!', types.identifier(infiniteVars.hasMoreVar), true)
|
|
2936
|
+
setJSXExpressionAttribute(
|
|
2937
|
+
button,
|
|
2938
|
+
'disabled',
|
|
2939
|
+
hasLoadingState
|
|
2940
|
+
? types.logicalExpression(
|
|
2941
|
+
'||',
|
|
2942
|
+
notHasMore,
|
|
2943
|
+
types.identifier(getLoadingStateVars(usage.index).isFetchingVar)
|
|
2944
|
+
)
|
|
2945
|
+
: notHasMore
|
|
2946
|
+
)
|
|
2947
|
+
}
|
|
2547
2948
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2949
|
+
/**
|
|
2950
|
+
* Wires one mapper's pagination container according to the mode it is in, and
|
|
2951
|
+
* removes the controls belonging to the other modes.
|
|
2952
|
+
*
|
|
2953
|
+
* The builder never deletes a control when the author switches mode — doing so
|
|
2954
|
+
* would throw away their styling — so the container can hold a superset of what
|
|
2955
|
+
* any one mode needs. Pruning here is what keeps a Previous/Next list from
|
|
2956
|
+
* rendering a dead numbered strip, and an infinite list from rendering page
|
|
2957
|
+
* buttons beside its Load More.
|
|
2958
|
+
*/
|
|
2959
|
+
function wirePaginationWidget(
|
|
2960
|
+
paginationNode: any,
|
|
2961
|
+
usage: DataSourceUsage,
|
|
2962
|
+
vars: ReturnType<typeof getStateVarsForUsage>,
|
|
2963
|
+
hasLoadingState: boolean
|
|
2964
|
+
): void {
|
|
2965
|
+
if (usage.infiniteScroll) {
|
|
2966
|
+
// Only the Load More button survives; page controls make no sense for a
|
|
2967
|
+
// list the visitor never leaves.
|
|
2968
|
+
dropInactivePaginationControls(paginationNode, ['load-more'])
|
|
2969
|
+
wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState)
|
|
2970
|
+
return
|
|
2971
|
+
}
|
|
2557
2972
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2973
|
+
const activeKinds: PaginationControlKind[] =
|
|
2974
|
+
usage.paginationMode === 'numbered'
|
|
2975
|
+
? ['first', 'previous', 'pages', 'page', 'ellipsis', 'next', 'last']
|
|
2976
|
+
: ['previous', 'next']
|
|
2977
|
+
dropInactivePaginationControls(paginationNode, activeKinds)
|
|
2978
|
+
|
|
2979
|
+
wirePaginationButtons(paginationNode, usage, vars)
|
|
2980
|
+
|
|
2981
|
+
if (usage.paginationMode === 'numbered') {
|
|
2982
|
+
const { currentPageExpr, buildSetPageStatement } = getPageAccessors(usage, vars)
|
|
2983
|
+
const numberedVars: NumberedPaginationVars = {
|
|
2984
|
+
index: usage.index,
|
|
2985
|
+
maxPagesVar: vars.maxPagesStateVar,
|
|
2986
|
+
currentPageExpr,
|
|
2987
|
+
buildSetPageStatement,
|
|
2565
2988
|
}
|
|
2566
|
-
|
|
2989
|
+
wireNumberedPagination(paginationNode, numberedVars)
|
|
2567
2990
|
}
|
|
2991
|
+
}
|
|
2568
2992
|
|
|
2569
|
-
|
|
2570
|
-
|
|
2993
|
+
function wirePaginationButtons(
|
|
2994
|
+
paginationNode: any,
|
|
2995
|
+
usage: DataSourceUsage,
|
|
2996
|
+
vars: ReturnType<typeof getStateVarsForUsage>
|
|
2997
|
+
): void {
|
|
2998
|
+
// Marker attribute first, class-name substring second — projects generated
|
|
2999
|
+
// before the marker existed carry only the class name.
|
|
3000
|
+
const prevButton = findPaginationControl(paginationNode, 'previous', 'previous')
|
|
3001
|
+
const nextButton = findPaginationControl(paginationNode, 'next', 'next')
|
|
2571
3002
|
|
|
2572
3003
|
const isCombinedState = usage.category === 'paginated+search'
|
|
2573
3004
|
|
|
2574
3005
|
if (prevButton) {
|
|
2575
|
-
|
|
2576
|
-
prevButton.openingElement.name.name = 'button'
|
|
2577
|
-
if (prevButton.closingElement) {
|
|
2578
|
-
prevButton.closingElement.name.name = 'button'
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
// Add type="button"
|
|
2582
|
-
const hasType = prevButton.openingElement.attributes.some((a: any) => a.name?.name === 'type')
|
|
2583
|
-
if (!hasType) {
|
|
2584
|
-
prevButton.openingElement.attributes.push(
|
|
2585
|
-
types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
|
|
2586
|
-
)
|
|
2587
|
-
}
|
|
3006
|
+
convertControlToButton(prevButton)
|
|
2588
3007
|
|
|
2589
3008
|
// Add onClick
|
|
2590
3009
|
prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(
|
|
@@ -2694,17 +3113,7 @@ function wirePaginationButtons(
|
|
|
2694
3113
|
}
|
|
2695
3114
|
|
|
2696
3115
|
if (nextButton) {
|
|
2697
|
-
nextButton
|
|
2698
|
-
if (nextButton.closingElement) {
|
|
2699
|
-
nextButton.closingElement.name.name = 'button'
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
const hasType = nextButton.openingElement.attributes.some((a: any) => a.name?.name === 'type')
|
|
2703
|
-
if (!hasType) {
|
|
2704
|
-
nextButton.openingElement.attributes.push(
|
|
2705
|
-
types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button'))
|
|
2706
|
-
)
|
|
2707
|
-
}
|
|
3116
|
+
convertControlToButton(nextButton)
|
|
2708
3117
|
|
|
2709
3118
|
nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(
|
|
2710
3119
|
(a: any) => a.name?.name !== 'onClick'
|