@teleporthq/teleport-plugin-next-data-source 0.42.34 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/__tests__/ecommerce-product-out-of-stock.test.ts +112 -0
  2. package/__tests__/fetchers.test.ts +0 -42
  3. package/__tests__/filter-utils.test.ts +149 -0
  4. package/__tests__/mocks.ts +0 -12
  5. package/__tests__/utils.test.ts +0 -2
  6. package/dist/cjs/array-mapper-registry.d.ts +2 -0
  7. package/dist/cjs/array-mapper-registry.d.ts.map +1 -1
  8. package/dist/cjs/array-mapper-registry.js +9 -1
  9. package/dist/cjs/array-mapper-registry.js.map +1 -1
  10. package/dist/cjs/count-fetchers.d.ts +2 -2
  11. package/dist/cjs/count-fetchers.d.ts.map +1 -1
  12. package/dist/cjs/count-fetchers.js +5 -5
  13. package/dist/cjs/count-fetchers.js.map +1 -1
  14. package/dist/cjs/data-source-fetchers.d.ts +2 -1
  15. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  16. package/dist/cjs/data-source-fetchers.js +11 -9
  17. package/dist/cjs/data-source-fetchers.js.map +1 -1
  18. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/airtable.js +1 -1
  20. package/dist/cjs/fetchers/airtable.js.map +1 -1
  21. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/clickhouse.js +1 -1
  23. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  24. package/dist/cjs/fetchers/csv-file.js +1 -1
  25. package/dist/cjs/fetchers/csv-file.js.map +1 -1
  26. package/dist/cjs/fetchers/firestore.js +1 -1
  27. package/dist/cjs/fetchers/firestore.js.map +1 -1
  28. package/dist/cjs/fetchers/google-sheets.js +1 -1
  29. package/dist/cjs/fetchers/google-sheets.js.map +1 -1
  30. package/dist/cjs/fetchers/index.d.ts +2 -1
  31. package/dist/cjs/fetchers/index.d.ts.map +1 -1
  32. package/dist/cjs/fetchers/index.js +8 -5
  33. package/dist/cjs/fetchers/index.js.map +1 -1
  34. package/dist/cjs/fetchers/javascript.js +1 -1
  35. package/dist/cjs/fetchers/javascript.js.map +1 -1
  36. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/mariadb.js +3 -3
  38. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  39. package/dist/cjs/fetchers/mongodb.js +1 -1
  40. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  41. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  42. package/dist/cjs/fetchers/mysql.js +2 -2
  43. package/dist/cjs/fetchers/mysql.js.map +1 -1
  44. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  45. package/dist/cjs/fetchers/postgresql.js +2 -2
  46. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  47. package/dist/cjs/fetchers/raw-query.d.ts +18 -0
  48. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -0
  49. package/dist/cjs/fetchers/raw-query.js +70 -0
  50. package/dist/cjs/fetchers/raw-query.js.map +1 -0
  51. package/dist/cjs/fetchers/redis.js +1 -1
  52. package/dist/cjs/fetchers/redis.js.map +1 -1
  53. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  54. package/dist/cjs/fetchers/redshift.js +2 -2
  55. package/dist/cjs/fetchers/redshift.js.map +1 -1
  56. package/dist/cjs/fetchers/rest-api.js +1 -1
  57. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  58. package/dist/cjs/fetchers/supabase.d.ts.map +1 -1
  59. package/dist/cjs/fetchers/supabase.js +62 -2
  60. package/dist/cjs/fetchers/supabase.js.map +1 -1
  61. package/dist/cjs/fetchers/teleport.d.ts +7 -0
  62. package/dist/cjs/fetchers/teleport.d.ts.map +1 -0
  63. package/dist/cjs/fetchers/teleport.js +63 -0
  64. package/dist/cjs/fetchers/teleport.js.map +1 -0
  65. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  66. package/dist/cjs/fetchers/turso.js +1 -1
  67. package/dist/cjs/fetchers/turso.js.map +1 -1
  68. package/dist/cjs/filter-utils.d.ts +13 -0
  69. package/dist/cjs/filter-utils.d.ts.map +1 -0
  70. package/dist/cjs/filter-utils.js +95 -0
  71. package/dist/cjs/filter-utils.js.map +1 -0
  72. package/dist/cjs/index.d.ts.map +1 -1
  73. package/dist/cjs/index.js +112 -9
  74. package/dist/cjs/index.js.map +1 -1
  75. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  76. package/dist/cjs/pagination-plugin.js +389 -128
  77. package/dist/cjs/pagination-plugin.js.map +1 -1
  78. package/dist/cjs/sort-utils.d.ts +10 -0
  79. package/dist/cjs/sort-utils.d.ts.map +1 -0
  80. package/dist/cjs/sort-utils.js +141 -0
  81. package/dist/cjs/sort-utils.js.map +1 -0
  82. package/dist/cjs/transformations/blog-post.d.ts +7 -0
  83. package/dist/cjs/transformations/blog-post.d.ts.map +1 -0
  84. package/dist/cjs/transformations/blog-post.js +13 -0
  85. package/dist/cjs/transformations/blog-post.js.map +1 -0
  86. package/dist/cjs/transformations/ecommerce-product.d.ts +7 -0
  87. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -0
  88. package/dist/cjs/transformations/ecommerce-product.js +13 -0
  89. package/dist/cjs/transformations/ecommerce-product.js.map +1 -0
  90. package/dist/cjs/transformations/index.d.ts +26 -0
  91. package/dist/cjs/transformations/index.d.ts.map +1 -0
  92. package/dist/cjs/transformations/index.js +81 -0
  93. package/dist/cjs/transformations/index.js.map +1 -0
  94. package/dist/cjs/transformations/shared-utils.d.ts +7 -0
  95. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -0
  96. package/dist/cjs/transformations/shared-utils.js +13 -0
  97. package/dist/cjs/transformations/shared-utils.js.map +1 -0
  98. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  99. package/dist/cjs/utils.d.ts +30 -1
  100. package/dist/cjs/utils.d.ts.map +1 -1
  101. package/dist/cjs/utils.js +173 -10
  102. package/dist/cjs/utils.js.map +1 -1
  103. package/dist/esm/array-mapper-registry.d.ts +2 -0
  104. package/dist/esm/array-mapper-registry.d.ts.map +1 -1
  105. package/dist/esm/array-mapper-registry.js +9 -1
  106. package/dist/esm/array-mapper-registry.js.map +1 -1
  107. package/dist/esm/count-fetchers.d.ts +2 -2
  108. package/dist/esm/count-fetchers.d.ts.map +1 -1
  109. package/dist/esm/count-fetchers.js +4 -4
  110. package/dist/esm/count-fetchers.js.map +1 -1
  111. package/dist/esm/data-source-fetchers.d.ts +2 -1
  112. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  113. package/dist/esm/data-source-fetchers.js +10 -9
  114. package/dist/esm/data-source-fetchers.js.map +1 -1
  115. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  116. package/dist/esm/fetchers/airtable.js +1 -1
  117. package/dist/esm/fetchers/airtable.js.map +1 -1
  118. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  119. package/dist/esm/fetchers/clickhouse.js +1 -1
  120. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  121. package/dist/esm/fetchers/csv-file.js +1 -1
  122. package/dist/esm/fetchers/csv-file.js.map +1 -1
  123. package/dist/esm/fetchers/firestore.js +1 -1
  124. package/dist/esm/fetchers/firestore.js.map +1 -1
  125. package/dist/esm/fetchers/google-sheets.js +1 -1
  126. package/dist/esm/fetchers/google-sheets.js.map +1 -1
  127. package/dist/esm/fetchers/index.d.ts +2 -1
  128. package/dist/esm/fetchers/index.d.ts.map +1 -1
  129. package/dist/esm/fetchers/index.js +2 -1
  130. package/dist/esm/fetchers/index.js.map +1 -1
  131. package/dist/esm/fetchers/javascript.js +1 -1
  132. package/dist/esm/fetchers/javascript.js.map +1 -1
  133. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  134. package/dist/esm/fetchers/mariadb.js +4 -4
  135. package/dist/esm/fetchers/mariadb.js.map +1 -1
  136. package/dist/esm/fetchers/mongodb.js +1 -1
  137. package/dist/esm/fetchers/mongodb.js.map +1 -1
  138. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  139. package/dist/esm/fetchers/mysql.js +3 -3
  140. package/dist/esm/fetchers/mysql.js.map +1 -1
  141. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  142. package/dist/esm/fetchers/postgresql.js +3 -3
  143. package/dist/esm/fetchers/postgresql.js.map +1 -1
  144. package/dist/esm/fetchers/raw-query.d.ts +18 -0
  145. package/dist/esm/fetchers/raw-query.d.ts.map +1 -0
  146. package/dist/esm/fetchers/raw-query.js +65 -0
  147. package/dist/esm/fetchers/raw-query.js.map +1 -0
  148. package/dist/esm/fetchers/redis.js +1 -1
  149. package/dist/esm/fetchers/redis.js.map +1 -1
  150. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  151. package/dist/esm/fetchers/redshift.js +3 -3
  152. package/dist/esm/fetchers/redshift.js.map +1 -1
  153. package/dist/esm/fetchers/rest-api.js +1 -1
  154. package/dist/esm/fetchers/rest-api.js.map +1 -1
  155. package/dist/esm/fetchers/supabase.d.ts.map +1 -1
  156. package/dist/esm/fetchers/supabase.js +63 -3
  157. package/dist/esm/fetchers/supabase.js.map +1 -1
  158. package/dist/esm/fetchers/teleport.d.ts +7 -0
  159. package/dist/esm/fetchers/teleport.d.ts.map +1 -0
  160. package/dist/esm/fetchers/teleport.js +57 -0
  161. package/dist/esm/fetchers/teleport.js.map +1 -0
  162. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  163. package/dist/esm/fetchers/turso.js +2 -2
  164. package/dist/esm/fetchers/turso.js.map +1 -1
  165. package/dist/esm/filter-utils.d.ts +13 -0
  166. package/dist/esm/filter-utils.d.ts.map +1 -0
  167. package/dist/esm/filter-utils.js +66 -0
  168. package/dist/esm/filter-utils.js.map +1 -0
  169. package/dist/esm/index.d.ts.map +1 -1
  170. package/dist/esm/index.js +113 -10
  171. package/dist/esm/index.js.map +1 -1
  172. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  173. package/dist/esm/pagination-plugin.js +389 -128
  174. package/dist/esm/pagination-plugin.js.map +1 -1
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -0
  177. package/dist/esm/sort-utils.js +113 -0
  178. package/dist/esm/sort-utils.js.map +1 -0
  179. package/dist/esm/transformations/blog-post.d.ts +7 -0
  180. package/dist/esm/transformations/blog-post.d.ts.map +1 -0
  181. package/dist/esm/transformations/blog-post.js +9 -0
  182. package/dist/esm/transformations/blog-post.js.map +1 -0
  183. package/dist/esm/transformations/ecommerce-product.d.ts +7 -0
  184. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -0
  185. package/dist/esm/transformations/ecommerce-product.js +9 -0
  186. package/dist/esm/transformations/ecommerce-product.js.map +1 -0
  187. package/dist/esm/transformations/index.d.ts +26 -0
  188. package/dist/esm/transformations/index.d.ts.map +1 -0
  189. package/dist/esm/transformations/index.js +74 -0
  190. package/dist/esm/transformations/index.js.map +1 -0
  191. package/dist/esm/transformations/shared-utils.d.ts +7 -0
  192. package/dist/esm/transformations/shared-utils.d.ts.map +1 -0
  193. package/dist/esm/transformations/shared-utils.js +9 -0
  194. package/dist/esm/transformations/shared-utils.js.map +1 -0
  195. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  196. package/dist/esm/utils.d.ts +30 -1
  197. package/dist/esm/utils.d.ts.map +1 -1
  198. package/dist/esm/utils.js +170 -9
  199. package/dist/esm/utils.js.map +1 -1
  200. package/package.json +6 -5
  201. package/src/array-mapper-registry.ts +13 -0
  202. package/src/count-fetchers.ts +5 -5
  203. package/src/data-source-fetchers.ts +15 -11
  204. package/src/fetchers/airtable.ts +54 -8
  205. package/src/fetchers/clickhouse.ts +25 -19
  206. package/src/fetchers/csv-file.ts +2 -2
  207. package/src/fetchers/firestore.ts +2 -2
  208. package/src/fetchers/google-sheets.ts +2 -2
  209. package/src/fetchers/index.ts +6 -5
  210. package/src/fetchers/javascript.ts +2 -2
  211. package/src/fetchers/mariadb.ts +27 -12
  212. package/src/fetchers/mongodb.ts +2 -2
  213. package/src/fetchers/mysql.ts +27 -12
  214. package/src/fetchers/postgresql.ts +31 -18
  215. package/src/fetchers/raw-query.ts +178 -0
  216. package/src/fetchers/redis.ts +2 -2
  217. package/src/fetchers/redshift.ts +14 -10
  218. package/src/fetchers/rest-api.ts +2 -2
  219. package/src/fetchers/supabase.ts +97 -14
  220. package/src/fetchers/teleport.ts +485 -0
  221. package/src/fetchers/turso.ts +15 -7
  222. package/src/filter-utils.ts +111 -0
  223. package/src/index.ts +146 -6
  224. package/src/pagination-plugin.ts +547 -308
  225. package/src/sort-utils.ts +150 -0
  226. package/src/transformations/blog-post.ts +128 -0
  227. package/src/transformations/ecommerce-product.ts +173 -0
  228. package/src/transformations/index.ts +97 -0
  229. package/src/transformations/shared-utils.ts +271 -0
  230. package/src/utils.ts +227 -11
  231. package/dist/cjs/fetchers/static-collection.d.ts +0 -7
  232. package/dist/cjs/fetchers/static-collection.d.ts.map +0 -1
  233. package/dist/cjs/fetchers/static-collection.js +0 -25
  234. package/dist/cjs/fetchers/static-collection.js.map +0 -1
  235. package/dist/esm/fetchers/static-collection.d.ts +0 -7
  236. package/dist/esm/fetchers/static-collection.d.ts.map +0 -1
  237. package/dist/esm/fetchers/static-collection.js +0 -19
  238. package/dist/esm/fetchers/static-collection.js.map +0 -1
  239. package/src/fetchers/static-collection.ts +0 -231
@@ -5,10 +5,64 @@ import {
5
5
  FileType,
6
6
  } from '@teleporthq/teleport-types'
7
7
  import * as types from '@babel/types'
8
+ import { parseExpression } from '@babel/parser'
8
9
  import { StringUtils } from '@teleporthq/teleport-shared'
9
10
  import { ASTUtils } from '@teleporthq/teleport-plugin-common'
10
11
  import { generateSafeFileName } from './utils'
11
12
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
+ import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
14
+ import { appendFiltersParam, pushStateIdsAsDeps } from './filter-utils'
15
+
16
+ // ----- searchDefaultValue support -----
17
+ //
18
+ // `searchDefaultValue` on a `cms-list-repeater` UIDL node seeds the
19
+ // generated search input and the pre-debounce combined state. UIDL
20
+ // stores the attribute as either `{type:'static',content:string}` (a
21
+ // hard-coded initial value) or `{type:'expr',content:string}` (a JS
22
+ // expression referencing props / state / router query / a URL param
23
+ // helper). Static strings become `types.stringLiteral(...)` and
24
+ // expressions are parsed via `@babel/parser.parseExpression` into a
25
+ // real AST node we slot straight into `useState(...)`.
26
+ type SearchDefaultValue =
27
+ | { kind: 'static'; value: string }
28
+ | { kind: 'expression'; ast: types.Expression }
29
+
30
+ // tslint:disable-next-line:no-any
31
+ function parseSearchDefaultValue(raw: any): SearchDefaultValue | undefined {
32
+ if (!raw || typeof raw !== 'object') {
33
+ return undefined
34
+ }
35
+ if (raw.type === 'static' && typeof raw.content === 'string' && raw.content.length > 0) {
36
+ return { kind: 'static', value: raw.content }
37
+ }
38
+ if ((raw.type === 'expr' || raw.type === 'dynamic') && typeof raw.content === 'string') {
39
+ const src = raw.content.trim()
40
+ if (src.length === 0) {
41
+ return undefined
42
+ }
43
+ try {
44
+ const ast = parseExpression(src, { sourceType: 'module', plugins: ['jsx'] })
45
+ return { kind: 'expression', ast: ast as types.Expression }
46
+ } catch {
47
+ // Fall back to no seed if the expression is malformed — better to
48
+ // render an empty input than to emit broken code.
49
+ return undefined
50
+ }
51
+ }
52
+ return undefined
53
+ }
54
+
55
+ function searchDefaultValueInitAST(value: SearchDefaultValue | undefined): types.Expression {
56
+ if (!value) {
57
+ return types.stringLiteral('')
58
+ }
59
+ if (value.kind === 'static') {
60
+ return types.stringLiteral(value.value)
61
+ }
62
+ // Clone so multiple `useState(...)` call sites each get an
63
+ // independent AST node — Babel does not support shared references.
64
+ return types.cloneNode(value.ast, /* deep */ true) as types.Expression
65
+ }
12
66
 
13
67
  // ==================== UIDL-FIRST STATE MANAGEMENT ====================
14
68
  // This module uses a UIDL-first approach: we scan the UIDL FIRST to identify
@@ -34,16 +88,37 @@ interface DataSourceUsage {
34
88
  // Search config
35
89
  searchEnabled: boolean
36
90
  searchDebounce: number
91
+ // Initial value for the search input. Static strings are seeded as
92
+ // `useState(<string>)`; dynamic UIDL expressions are parsed via
93
+ // `@babel/parser` and slotted in as real AST nodes so the generated
94
+ // component can reference props / state / router-query / URL-param
95
+ // helpers in the initial value.
96
+ searchDefaultValue?: SearchDefaultValue
37
97
  // Query columns from resource params
38
98
  queryColumns: string[]
39
- // Sorts from resource params
99
+ // Sorts from resource params (legacy static-array form)
40
100
  // tslint:disable-next-line:no-any
41
101
  sorts: any[]
42
- // Filters from resource params
102
+ // Dynamic single-column sort bound to component state (set when legacy sorts empty
103
+ // and cms-list-repeater declares sort/sortDirection fields)
104
+ dynamicSort?: DynamicSortAST
105
+ // Filters from resource params (FLAT condition array — any `{ type: 'group',
106
+ // children: [...] }` wrappers from the UIDL inspector have already been
107
+ // unwrapped via `flattenFilterGroups`). Downstream emit sites rely on
108
+ // entries having `.source` / `.destination` / `.operand` directly.
43
109
  // tslint:disable-next-line:no-any
44
110
  filters: any[]
45
- // State IDs from dynamic filter destinations (for useMemo dependencies)
111
+ // State IDs from dynamic filter destinations (for useMemo dependencies).
112
+ // Only state references land here; `urlSearchParams` refs are tracked in
113
+ // `filterUrlSearchParamKeys` because their dep expression is
114
+ // `router.query.<key>`, not a bare identifier.
46
115
  filterStateIds: string[]
116
+ // URL search-param keys referenced by filter destinations (e.g.
117
+ // `'categoryFilter'`). Drives `const router = useRouter()` injection and
118
+ // the corresponding `router.query.<key>` entries in `useMemo` deps so the
119
+ // client-side fetch refires when the buyer navigates between
120
+ // `?categoryFilter=Rings` and `?categoryFilter=Necklaces`.
121
+ filterUrlSearchParamKeys: string[]
47
122
  // Computed category
48
123
  category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
49
124
  }
@@ -56,6 +131,205 @@ interface StateRegistry {
56
131
  byArrayMapperRenderProp: Map<string, DataSourceUsage>
57
132
  }
58
133
 
134
+ // The UIDL's `filters.content` array can wrap one or more conditions inside
135
+ // a `{ type: 'group', operator: 'and' | 'or', children: [...] }` entry — the
136
+ // GUI builds this shape when the inspector adds a logical group. The data
137
+ // source API endpoint (`processFilters` in the generated data-source module)
138
+ // only knows how to consume a FLAT array of `{ source, destination, operand }`
139
+ // conditions, so we walk groups recursively here and collect their leaf
140
+ // conditions in order. Anything that isn't a group AND isn't a condition is
141
+ // dropped — defensive against partially-built filter entries that would
142
+ // otherwise emit `{ source: '', destination: '', operand: '' }` rows the
143
+ // API ignores anyway.
144
+ //
145
+ // `or`-grouped conditions are flattened just like `and`-grouped ones — the
146
+ // downstream SQL builder always joins flat filters with `AND`, so emitting
147
+ // the conditions side-by-side approximates AND semantics. A future API
148
+ // upgrade that respects group operators would key off the original tree
149
+ // directly; until then, AND-flattening is the closest correct behaviour and
150
+ // matches what the inspector preview shows for the common single-group case
151
+ // the GUI emits today.
152
+ function flattenFilterGroups(filters: any[]): any[] {
153
+ if (!Array.isArray(filters)) {
154
+ return []
155
+ }
156
+ const out: any[] = []
157
+ const walk = (entry: any): void => {
158
+ if (!entry || typeof entry !== 'object') {
159
+ return
160
+ }
161
+ if (entry.type === 'group' && Array.isArray(entry.children)) {
162
+ for (const child of entry.children) {
163
+ walk(child)
164
+ }
165
+ return
166
+ }
167
+ // Backwards-compatible: entries without an explicit `type` (legacy flat
168
+ // form) and entries explicitly tagged `condition` are both treated as
169
+ // condition leaves.
170
+ if (entry.type === undefined || entry.type === 'condition') {
171
+ out.push(entry)
172
+ }
173
+ }
174
+ for (const entry of filters) {
175
+ walk(entry)
176
+ }
177
+ return out
178
+ }
179
+
180
+ // Walks a flat filter list to extract every `urlSearchParams` reference key
181
+ // (e.g. `'categoryFilter'`). Used to (a) inject `const router = useRouter()`
182
+ // once at the top of the component, (b) wire `router.query.<key>` into the
183
+ // `useMemo` dependency array so the client-side fetch reruns whenever the
184
+ // buyer navigates to a URL with a different `?key=value`. Returned in the
185
+ // order keys first appear so the generated dep array stays stable.
186
+ function collectFilterUrlSearchParamKeys(filters: any[]): string[] {
187
+ const seen = new Set<string>()
188
+ const out: string[] = []
189
+ for (const f of filters) {
190
+ const dest = f?.destination
191
+ if (!ASTUtils.isUIDLDynamicReference(dest)) {
192
+ continue
193
+ }
194
+ const content = (dest as { content?: { referenceType?: string; id?: string } }).content
195
+ if (!content || content.referenceType !== 'urlSearchParams' || !content.id) {
196
+ continue
197
+ }
198
+ if (seen.has(content.id)) {
199
+ continue
200
+ }
201
+ seen.add(content.id)
202
+ out.push(content.id)
203
+ }
204
+ return out
205
+ }
206
+
207
+ // Returns true when any filter destination is a `urlSearchParams` dynamic
208
+ // reference — used by the plugin to know it needs to import `useRouter` and
209
+ // emit `const router = useRouter()` at the top of the component. Cheaper to
210
+ // test against the precomputed key list once than to walk all filters on
211
+ // every check.
212
+ function hasUrlSearchParamFilters(usage: DataSourceUsage): boolean {
213
+ return usage.filterUrlSearchParamKeys.length > 0
214
+ }
215
+
216
+ // Appends `router.query.<key>` member expressions to a useMemo deps array so
217
+ // every client-side fetch refires when the buyer navigates between URLs
218
+ // that differ only by `?key=value`. React's shallow-compare semantics treat
219
+ // the member expression as a distinct value per render, so the array stays
220
+ // stable across paints with the same query string and changes the moment
221
+ // the URL does. Skip the bare-identifier dedupe `filterStateIds` uses —
222
+ // member expressions never collide with state identifiers, and the deps
223
+ // array allows duplicates without harm.
224
+ function pushUrlSearchParamMemoDeps(memoDeps: types.Expression[], usage: DataSourceUsage): void {
225
+ for (const key of usage.filterUrlSearchParamKeys) {
226
+ memoDeps.push(
227
+ types.memberExpression(
228
+ types.memberExpression(types.identifier('router'), types.identifier('query')),
229
+ types.identifier(key)
230
+ )
231
+ )
232
+ }
233
+ }
234
+
235
+ // Builds the AST for `!router.query.<key1> && !router.query.<key2> && ...`,
236
+ // used as a runtime guard around `initialData={props.X}` so the server-
237
+ // prefetched (unfiltered) data is only handed to `DataProvider` when the
238
+ // URL has no active filter. Without this guard, navigating to
239
+ // `/products-list?categoryFilter=Rings` (especially via soft Next.js
240
+ // transitions where the page component remounts with fresh getStaticProps
241
+ // but the buyer's URL filter is still in scope) shows the unfiltered list
242
+ // for the first paint AND keeps it on screen because `DataProvider`'s
243
+ // `passFetchBecauseWeHaveInitialData` ref skips the very first fetch when
244
+ // `initialData !== undefined`. By emitting `undefined` here whenever ANY
245
+ // url-search-param filter is set, the DataProvider's mount-time fetch runs
246
+ // immediately with the filtered params instead of presenting stale data.
247
+ //
248
+ // Returns `null` when the usage has no url-search-param filters at all —
249
+ // callers fall back to the existing `props.X` expression unchanged so
250
+ // non-filtered pages still benefit from the SSR prefetch.
251
+ function buildNoUrlFilterGuard(usage: DataSourceUsage): types.Expression | null {
252
+ if (usage.filterUrlSearchParamKeys.length === 0 && usage.filterStateIds.length === 0) {
253
+ return null
254
+ }
255
+ const guards: types.Expression[] = []
256
+ for (const key of usage.filterUrlSearchParamKeys) {
257
+ guards.push(
258
+ types.unaryExpression(
259
+ '!',
260
+ types.memberExpression(
261
+ types.memberExpression(types.identifier('router'), types.identifier('query')),
262
+ types.identifier(key)
263
+ ),
264
+ true
265
+ )
266
+ )
267
+ }
268
+ // State-bound filter destinations (e.g. `selectedCategory`) emit as bare
269
+ // identifiers, so the corresponding guard is `!selectedCategory`. An empty
270
+ // string ('') for the state — which the GUI emits when the user picks the
271
+ // "All Categories" reset option — is falsy and so passes through the guard
272
+ // exactly like a missing URL param: initialData (unfiltered prefetch) wins
273
+ // until the user picks a real value.
274
+ for (const id of usage.filterStateIds) {
275
+ guards.push(types.unaryExpression('!', types.identifier(id), true))
276
+ }
277
+ return guards.reduce((acc, next) => types.logicalExpression('&&', acc, next))
278
+ }
279
+
280
+ // Wraps an existing `initialData` condition with the additional "no URL
281
+ // filter active" guard so server-prefetched data is only handed to
282
+ // DataProvider when both (a) the original guard (page === 1, no search
283
+ // query, etc.) AND (b) every relevant `router.query.<key>` is falsy. See
284
+ // `buildNoUrlFilterGuard` for the rationale on why bare-identity feature
285
+ // detection is safer than the `dynamicSort`-style "always undefined" path.
286
+ function wrapInitialDataWithUrlFilterGuard(
287
+ baseCondition: types.Expression,
288
+ usage: DataSourceUsage
289
+ ): types.Expression {
290
+ const noFilterGuard = buildNoUrlFilterGuard(usage)
291
+ if (!noFilterGuard) {
292
+ return baseCondition
293
+ }
294
+ return types.logicalExpression('&&', baseCondition, noFilterGuard)
295
+ }
296
+
297
+ // Builds the destination AST for a single filter entry. Replaces the bare
298
+ // `ASTUtils.convertFilterDestinationToExpression(filter.destination)` call
299
+ // at every emit site so `urlSearchParams` references resolve to
300
+ // `router?.query?.<key>` instead of falling through to a bare identifier
301
+ // (which the existing helper would emit, leaving the client-side fetch
302
+ // referencing an undeclared symbol).
303
+ //
304
+ // State and prop references delegate back to the shared helper so the
305
+ // existing inspector behaviour (state-bound filter destinations) keeps
306
+ // working unchanged. `router?.query?.<key>` is the same shape the
307
+ // `createNextUrlSearchParamsPlugin`-driven `dynamicReferencePrefixMap`
308
+ // emits for page-level navlink reads.
309
+ function buildFilterDestinationExpression(destination: unknown): types.Expression {
310
+ if (ASTUtils.isUIDLDynamicReference(destination)) {
311
+ const content = (destination as { content?: { referenceType?: string; id?: string } }).content
312
+ if (content?.referenceType === 'urlSearchParams' && content?.id) {
313
+ // router?.query?.<id> — the optional-chain survives the first paint
314
+ // where Next.js's `useRouter()` returns `null` during static export
315
+ // hydration, so the fetch doesn't crash with "Cannot read properties
316
+ // of null" before the router is ready.
317
+ return types.optionalMemberExpression(
318
+ types.optionalMemberExpression(
319
+ types.identifier('router'),
320
+ types.identifier('query'),
321
+ false,
322
+ true
323
+ ),
324
+ types.identifier(content.id),
325
+ false,
326
+ true
327
+ )
328
+ }
329
+ }
330
+ return ASTUtils.convertFilterDestinationToExpression(destination)
331
+ }
332
+
59
333
  // Scan UIDL to find all data source usages and build a registry
60
334
  function buildStateRegistry(uidlNode: any): StateRegistry {
61
335
  const usages: DataSourceUsage[] = []
@@ -113,22 +387,52 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
113
387
  queryColumns = parentDataSource.resourceParams.queryColumns.content
114
388
  }
115
389
 
116
- // Extract sorts from parent's resource params
390
+ // Extract sorts from parent's resource params (legacy static array form)
117
391
  let sorts: any[] = []
118
392
  if (parentDataSource.resourceParams?.sorts?.content) {
119
393
  sorts = parentDataSource.resourceParams.sorts.content
120
394
  }
121
395
 
122
- // Extract filters from parent's resource params
396
+ // If legacy sorts aren't set, fall back to the new dynamic single-column
397
+ // sort fields on the cms-list-repeater (used by admin-panel listing pages).
398
+ let dynamicSort: DynamicSortAST | undefined
399
+ if ((!sorts || sorts.length === 0) && content.sort) {
400
+ dynamicSort = extractDynamicSort(content.sort, content.sortDirection)
401
+ }
402
+
403
+ // Extract filters from parent's resource params. The inspector wraps
404
+ // every condition in a `{ type: 'group' }` envelope (single-group or
405
+ // nested), so flatten to leaf conditions before the downstream emit
406
+ // sites consume `.source` / `.destination` / `.operand` directly —
407
+ // they expect a flat condition array. See `flattenFilterGroups`'s
408
+ // header comment for why AND-flattening is the correct fallback for
409
+ // the API endpoint's flat-condition contract.
123
410
  let filters: any[] = []
124
411
  if (parentDataSource.resourceParams?.filters?.content) {
125
- filters = parentDataSource.resourceParams.filters.content
412
+ filters = flattenFilterGroups(parentDataSource.resourceParams.filters.content)
126
413
  }
127
414
 
128
- // Extract state IDs from dynamic filter destinations
129
- const filterStateIds: string[] = filters
130
- .filter((f) => ASTUtils.isUIDLDynamicReference(f.destination))
131
- .map((f) => (f.destination as { content: { id: string } }).content.id)
415
+ // Split dynamic destination keys by reference type. `state`/`prop`
416
+ // refs resolve to bare identifiers (so they're useMemo deps as-is);
417
+ // `urlSearchParams` refs resolve to `router.query.<key>` and need a
418
+ // `useRouter()` declaration injected separately.
419
+ const filterStateIds: string[] = []
420
+ for (const f of filters) {
421
+ if (!ASTUtils.isUIDLDynamicReference(f.destination)) {
422
+ continue
423
+ }
424
+ const destinationContent = (
425
+ f.destination as { content?: { referenceType?: string; id?: string } }
426
+ ).content
427
+ if (!destinationContent || !destinationContent.id) {
428
+ continue
429
+ }
430
+ if (destinationContent.referenceType === 'urlSearchParams') {
431
+ continue
432
+ }
433
+ filterStateIds.push(destinationContent.id)
434
+ }
435
+ const filterUrlSearchParamKeys: string[] = collectFilterUrlSearchParamKeys(filters)
132
436
 
133
437
  // Extract limit from parent's resource params (for plain array mappers)
134
438
  let limit = 0
@@ -140,6 +444,8 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
140
444
  // For plain mappers, use limit from data-source-list resource params
141
445
  const effectivePerPage = content.paginated ? content.perPage : limit || content.perPage
142
446
 
447
+ const searchDefaultValue = parseSearchDefaultValue(content.searchDefaultValue)
448
+
143
449
  const usage: DataSourceUsage = {
144
450
  index: index++,
145
451
  dataSourceIdentifier: parentDataSource.identifier,
@@ -153,10 +459,13 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
153
459
  perPage: effectivePerPage,
154
460
  searchEnabled: !!content.searchEnabled,
155
461
  searchDebounce: content.searchDebounce || 300,
462
+ searchDefaultValue,
156
463
  queryColumns,
157
464
  sorts,
465
+ dynamicSort,
158
466
  filters,
159
467
  filterStateIds,
468
+ filterUrlSearchParamKeys,
160
469
  category: 'plain',
161
470
  }
162
471
 
@@ -419,14 +728,18 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
419
728
  types.callExpression(types.identifier('useState'), [
420
729
  types.objectExpression([
421
730
  types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
422
- types.objectProperty(types.identifier('debouncedQuery'), types.stringLiteral('')),
731
+ types.objectProperty(
732
+ types.identifier('debouncedQuery'),
733
+ searchDefaultValueInitAST(usage.searchDefaultValue)
734
+ ),
423
735
  ]),
424
736
  ])
425
737
  ),
426
738
  ])
427
739
  )
428
740
 
429
- // Immediate search query state
741
+ // Immediate search query state — seeded with `searchDefaultValue`
742
+ // when provided so the input is pre-filled on mount.
430
743
  stateDeclarations.push(
431
744
  types.variableDeclaration('const', [
432
745
  types.variableDeclarator(
@@ -434,7 +747,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
434
747
  types.identifier(vars.searchQueryVar),
435
748
  types.identifier(vars.setSearchQueryVar),
436
749
  ]),
437
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
750
+ types.callExpression(types.identifier('useState'), [
751
+ searchDefaultValueInitAST(usage.searchDefaultValue),
752
+ ])
438
753
  ),
439
754
  ])
440
755
  )
@@ -561,36 +876,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
561
876
  )
562
877
  }
563
878
  // Add filters to count fetch params if present
564
- if (usage.filters && usage.filters.length > 0) {
565
- urlParams.push(
566
- types.objectProperty(
567
- types.identifier('filters'),
568
- types.callExpression(
569
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
570
- [
571
- types.arrayExpression(
572
- usage.filters.map((filter: any) =>
573
- types.objectExpression([
574
- types.objectProperty(
575
- types.identifier('source'),
576
- types.stringLiteral(filter.source || '')
577
- ),
578
- types.objectProperty(
579
- types.identifier('destination'),
580
- ASTUtils.convertFilterDestinationToExpression(filter.destination)
581
- ),
582
- types.objectProperty(
583
- types.identifier('operand'),
584
- types.stringLiteral(filter.operand || '')
585
- ),
586
- ])
587
- )
588
- ),
589
- ]
590
- )
591
- )
592
- )
593
- }
879
+ appendFiltersParam(urlParams, usage.filters, buildFilterDestinationExpression)
594
880
 
595
881
  // Build the count fetch effect body
596
882
  const countFetchEffectBody: types.Statement[] = []
@@ -712,16 +998,26 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
712
998
  )
713
999
  )
714
1000
 
1001
+ const countEffectDeps: types.Expression[] = [
1002
+ types.memberExpression(
1003
+ types.identifier(vars.combinedStateVar),
1004
+ types.identifier('debouncedQuery')
1005
+ ),
1006
+ ]
1007
+ // Refresh the count whenever a state-bound filter destination changes
1008
+ // (e.g. user picks a category) so pagination tracks the filtered
1009
+ // result-set, not the mount-time unfiltered total. Without these
1010
+ // deps, ds_0_maxPages stays at the original count and the "Next"
1011
+ // button stays enabled past the actual last page of the filtered
1012
+ // results — letting the user click into empty pages.
1013
+ pushStateIdsAsDeps(countEffectDeps, new Set<string>(), usage.filterStateIds)
1014
+ // Same goes for URL-driven filters (already documented above).
1015
+ pushUrlSearchParamMemoDeps(countEffectDeps, usage)
715
1016
  effectStatements.push(
716
1017
  types.expressionStatement(
717
1018
  types.callExpression(types.identifier('useEffect'), [
718
1019
  types.arrowFunctionExpression([], types.blockStatement(countFetchEffectBody)),
719
- types.arrayExpression([
720
- types.memberExpression(
721
- types.identifier(vars.combinedStateVar),
722
- types.identifier('debouncedQuery')
723
- ),
724
- ]),
1020
+ types.arrayExpression(countEffectDeps),
725
1021
  ])
726
1022
  )
727
1023
  )
@@ -862,7 +1158,18 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
862
1158
  ),
863
1159
  ])
864
1160
  ),
865
- types.arrayExpression([]), // Empty dependency array - fetch on mount only
1161
+ // Default to mount-only; refresh when ANY filter destination
1162
+ // changes — state-bound (e.g. `selectedCategory`) and
1163
+ // URL-driven — so the pagination control reflects the current
1164
+ // filtered count instead of the unfiltered mount-time total.
1165
+ types.arrayExpression(
1166
+ ((): types.Expression[] => {
1167
+ const deps: types.Expression[] = []
1168
+ pushStateIdsAsDeps(deps, new Set<string>(), usage.filterStateIds)
1169
+ pushUrlSearchParamMemoDeps(deps, usage)
1170
+ return deps
1171
+ })()
1172
+ ),
866
1173
  ])
867
1174
  )
868
1175
  )
@@ -885,7 +1192,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
885
1192
  types.identifier(vars.debouncedSearchQueryVar),
886
1193
  types.identifier(vars.setDebouncedSearchQueryVar),
887
1194
  ]),
888
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1195
+ types.callExpression(types.identifier('useState'), [
1196
+ searchDefaultValueInitAST(usage.searchDefaultValue),
1197
+ ])
889
1198
  ),
890
1199
  ])
891
1200
  )
@@ -897,7 +1206,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
897
1206
  types.identifier(vars.searchQueryVar),
898
1207
  types.identifier(vars.setSearchQueryVar),
899
1208
  ]),
900
- types.callExpression(types.identifier('useState'), [types.stringLiteral('')])
1209
+ types.callExpression(types.identifier('useState'), [
1210
+ searchDefaultValueInitAST(usage.searchDefaultValue),
1211
+ ])
901
1212
  ),
902
1213
  ])
903
1214
  )
@@ -967,6 +1278,57 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
967
1278
  // Insert state declarations at the beginning
968
1279
  stateDeclarations.reverse().forEach((s) => blockStatement.body.unshift(s))
969
1280
 
1281
+ // Inject `const router = useRouter()` at the very top of the component
1282
+ // body whenever any usage's filters reference URL search params (e.g.
1283
+ // a navlink that bakes `?categoryFilter=Rings` into the href). The
1284
+ // filter destination expressions emitted earlier reference `router.query`
1285
+ // directly, so the symbol must be in scope by the time the component
1286
+ // body runs. We add the dependency + declaration here rather than
1287
+ // relying on the sibling `createNextUrlSearchParamsPlugin` because
1288
+ // (a) that plugin only fires when the UIDL declares `pageOptions.searchParams`,
1289
+ // and (b) component-level data fetches can use URL params even when
1290
+ // the page itself has no `searchParams` definition (e.g. a navigation
1291
+ // component embedded on a page that didn't author the param). The
1292
+ // `body.some(isUseRouterDecl)` guard keeps us idempotent with both the
1293
+ // search-params plugin and the i18n plugin, both of which may have
1294
+ // already unshifted the same declaration.
1295
+ const needsUseRouter = registry.usages.some((u) => hasUrlSearchParamFilters(u))
1296
+ if (needsUseRouter) {
1297
+ if (!dependencies.useRouter) {
1298
+ // Match the shape the sibling Next.js plugins use (i18n locale mapper,
1299
+ // search-params plugin) so the deduped import line is identical and
1300
+ // the dependency-resolver merges instead of emitting a second one.
1301
+ dependencies.useRouter = {
1302
+ type: 'library',
1303
+ path: 'next/router',
1304
+ version: '^12.1.10',
1305
+ meta: { namedImport: true },
1306
+ }
1307
+ }
1308
+ const hasRouterDecl = blockStatement.body.some(
1309
+ (statement) =>
1310
+ statement.type === 'VariableDeclaration' &&
1311
+ statement.declarations.some(
1312
+ (decl) =>
1313
+ decl.id.type === 'Identifier' &&
1314
+ decl.id.name === 'router' &&
1315
+ decl.init?.type === 'CallExpression' &&
1316
+ decl.init.callee.type === 'Identifier' &&
1317
+ decl.init.callee.name === 'useRouter'
1318
+ )
1319
+ )
1320
+ if (!hasRouterDecl) {
1321
+ blockStatement.body.unshift(
1322
+ types.variableDeclaration('const', [
1323
+ types.variableDeclarator(
1324
+ types.identifier('router'),
1325
+ types.callExpression(types.identifier('useRouter'), [])
1326
+ ),
1327
+ ])
1328
+ )
1329
+ }
1330
+ }
1331
+
970
1332
  // Insert effects before return statement
971
1333
  const returnIndex = blockStatement.body.findIndex((s: any) => s.type === 'ReturnStatement')
972
1334
  const insertIndex = returnIndex !== -1 ? returnIndex : blockStatement.body.length
@@ -1078,7 +1440,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1078
1440
 
1079
1441
  // STEP 6: Update getStaticProps if this is a page
1080
1442
  if (isPage) {
1081
- updateGetStaticProps(chunks, registry, dependencies)
1443
+ updateGetStaticProps(chunks, registry, dependencies, uidl.outputOptions?.folderPath)
1082
1444
  }
1083
1445
 
1084
1446
  return structure
@@ -1326,71 +1688,20 @@ function updateDataProviderForPaginatedSearch(
1326
1688
  )
1327
1689
  }
1328
1690
 
1329
- // Add sorts if present
1330
- if (usage.sorts && usage.sorts.length > 0) {
1331
- paramsProps.push(
1332
- types.objectProperty(
1333
- types.identifier('sorts'),
1334
- types.callExpression(
1335
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1336
- [
1337
- types.arrayExpression(
1338
- usage.sorts.map((sort: any) =>
1339
- types.objectExpression([
1340
- types.objectProperty(
1341
- types.identifier('field'),
1342
- types.stringLiteral(sort.field || '')
1343
- ),
1344
- types.objectProperty(
1345
- types.identifier('order'),
1346
- types.stringLiteral(sort.order || '')
1347
- ),
1348
- ])
1349
- )
1350
- ),
1351
- ]
1352
- )
1353
- )
1354
- )
1355
- }
1691
+ // Add sorts if present (legacy static array wins; otherwise dynamic state-bound sort)
1692
+ appendSortsParam(paramsProps, usage.sorts, usage.dynamicSort)
1356
1693
 
1357
1694
  // Add filters if present
1358
- if (usage.filters && usage.filters.length > 0) {
1359
- paramsProps.push(
1360
- types.objectProperty(
1361
- types.identifier('filters'),
1362
- types.callExpression(
1363
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1364
- [
1365
- types.arrayExpression(
1366
- usage.filters.map((filter: any) =>
1367
- types.objectExpression([
1368
- types.objectProperty(
1369
- types.identifier('source'),
1370
- types.stringLiteral(filter.source || '')
1371
- ),
1372
- types.objectProperty(
1373
- types.identifier('destination'),
1374
- ASTUtils.convertFilterDestinationToExpression(filter.destination)
1375
- ),
1376
- types.objectProperty(
1377
- types.identifier('operand'),
1378
- types.stringLiteral(filter.operand || '')
1379
- ),
1380
- ])
1381
- )
1382
- ),
1383
- ]
1384
- )
1385
- )
1386
- )
1387
- }
1695
+ appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
1388
1696
 
1389
- // Build useMemo dependencies including filter state IDs
1697
+ // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
1390
1698
  const memoDeps: types.Expression[] = [types.identifier(vars.combinedStateVar)]
1391
- usage.filterStateIds.forEach((stateId) => {
1392
- memoDeps.push(types.identifier(stateId))
1393
- })
1699
+ const seenDeps = new Set<string>([vars.combinedStateVar])
1700
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.filterStateIds)
1701
+ if (usage.dynamicSort) {
1702
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.dynamicSort.depStateIds)
1703
+ }
1704
+ pushUrlSearchParamMemoDeps(memoDeps, usage)
1394
1705
 
1395
1706
  dp.openingElement.attributes.push(
1396
1707
  types.jsxAttribute(
@@ -1404,42 +1715,56 @@ function updateDataProviderForPaginatedSearch(
1404
1715
  )
1405
1716
  )
1406
1717
 
1407
- // Add initialData
1408
- const initialDataCondition = types.logicalExpression(
1409
- '&&',
1410
- types.binaryExpression(
1411
- '===',
1412
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1413
- types.numericLiteral(1)
1414
- ),
1415
- types.unaryExpression(
1416
- '!',
1417
- types.memberExpression(
1418
- types.identifier(vars.combinedStateVar),
1419
- types.identifier('debouncedQuery')
1718
+ // Add initialData. Skip the server-prefetched data entirely when a dynamic
1719
+ // state-bound sort is active — the prefetch ran without sort parameters, so
1720
+ // reusing it would mask the current sort state AND cause DataProvider to skip
1721
+ // the first client fetch (its internal guard only skips when initialData is
1722
+ // defined on mount). Without that skip, toggling sort correctly triggers a
1723
+ // refetch via the useMemo params dependency chain.
1724
+ if (!usage.dynamicSort) {
1725
+ const initialDataCondition = wrapInitialDataWithUrlFilterGuard(
1726
+ types.logicalExpression(
1727
+ '&&',
1728
+ types.binaryExpression(
1729
+ '===',
1730
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1731
+ types.numericLiteral(1)
1732
+ ),
1733
+ types.unaryExpression(
1734
+ '!',
1735
+ types.memberExpression(
1736
+ types.identifier(vars.combinedStateVar),
1737
+ types.identifier('debouncedQuery')
1738
+ ),
1739
+ true
1740
+ )
1420
1741
  ),
1421
- true
1742
+ usage
1422
1743
  )
1423
- )
1424
- dp.openingElement.attributes.push(
1425
- types.jsxAttribute(
1426
- types.jsxIdentifier('initialData'),
1427
- types.jsxExpressionContainer(
1428
- types.conditionalExpression(
1429
- initialDataCondition,
1430
- types.optionalMemberExpression(
1431
- types.identifier('props'),
1432
- types.identifier(vars.propsPrefix),
1433
- false,
1434
- true
1435
- ),
1436
- types.identifier('undefined')
1744
+ dp.openingElement.attributes.push(
1745
+ types.jsxAttribute(
1746
+ types.jsxIdentifier('initialData'),
1747
+ types.jsxExpressionContainer(
1748
+ types.conditionalExpression(
1749
+ initialDataCondition,
1750
+ types.optionalMemberExpression(
1751
+ types.identifier('props'),
1752
+ types.identifier(vars.propsPrefix),
1753
+ false,
1754
+ true
1755
+ ),
1756
+ types.identifier('undefined')
1757
+ )
1437
1758
  )
1438
1759
  )
1439
1760
  )
1440
- )
1761
+ }
1441
1762
 
1442
- // Add key
1763
+ // Add key. Sort is intentionally NOT part of the key — a key change would
1764
+ // remount the DataProvider, and a fresh mount re-arms the internal
1765
+ // "skip-first-fetch-when-we-have-initialData" guard, which would prevent the
1766
+ // new sort params from reaching the fetcher. Leaving sort out of the key
1767
+ // lets the useMemo params identity change alone drive refetch.
1443
1768
  dp.openingElement.attributes.push(
1444
1769
  types.jsxAttribute(
1445
1770
  types.jsxIdentifier('key'),
@@ -1501,71 +1826,20 @@ function updateDataProviderForPaginationOnly(
1501
1826
  types.objectProperty(types.identifier('perPage'), types.numericLiteral(usage.perPage)),
1502
1827
  ]
1503
1828
 
1504
- // Add sorts if present
1505
- if (usage.sorts && usage.sorts.length > 0) {
1506
- paramsProps.push(
1507
- types.objectProperty(
1508
- types.identifier('sorts'),
1509
- types.callExpression(
1510
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1511
- [
1512
- types.arrayExpression(
1513
- usage.sorts.map((sort: any) =>
1514
- types.objectExpression([
1515
- types.objectProperty(
1516
- types.identifier('field'),
1517
- types.stringLiteral(sort.field || '')
1518
- ),
1519
- types.objectProperty(
1520
- types.identifier('order'),
1521
- types.stringLiteral(sort.order || '')
1522
- ),
1523
- ])
1524
- )
1525
- ),
1526
- ]
1527
- )
1528
- )
1529
- )
1530
- }
1829
+ // Add sorts if present (legacy static array wins; otherwise dynamic state-bound sort)
1830
+ appendSortsParam(paramsProps, usage.sorts, usage.dynamicSort)
1531
1831
 
1532
1832
  // Add filters if present
1533
- if (usage.filters && usage.filters.length > 0) {
1534
- paramsProps.push(
1535
- types.objectProperty(
1536
- types.identifier('filters'),
1537
- types.callExpression(
1538
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1539
- [
1540
- types.arrayExpression(
1541
- usage.filters.map((filter: any) =>
1542
- types.objectExpression([
1543
- types.objectProperty(
1544
- types.identifier('source'),
1545
- types.stringLiteral(filter.source || '')
1546
- ),
1547
- types.objectProperty(
1548
- types.identifier('destination'),
1549
- ASTUtils.convertFilterDestinationToExpression(filter.destination)
1550
- ),
1551
- types.objectProperty(
1552
- types.identifier('operand'),
1553
- types.stringLiteral(filter.operand || '')
1554
- ),
1555
- ])
1556
- )
1557
- ),
1558
- ]
1559
- )
1560
- )
1561
- )
1562
- }
1833
+ appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
1563
1834
 
1564
- // Build useMemo dependencies including filter state IDs
1835
+ // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
1565
1836
  const memoDeps: types.Expression[] = [types.identifier(vars.pageStateVar)]
1566
- usage.filterStateIds.forEach((stateId) => {
1567
- memoDeps.push(types.identifier(stateId))
1568
- })
1837
+ const seenDeps = new Set<string>([vars.pageStateVar])
1838
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.filterStateIds)
1839
+ if (usage.dynamicSort) {
1840
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.dynamicSort.depStateIds)
1841
+ }
1842
+ pushUrlSearchParamMemoDeps(memoDeps, usage)
1569
1843
 
1570
1844
  // Add params
1571
1845
  dp.openingElement.attributes.push(
@@ -1580,30 +1854,36 @@ function updateDataProviderForPaginationOnly(
1580
1854
  )
1581
1855
  )
1582
1856
 
1583
- // Add initialData
1584
- dp.openingElement.attributes.push(
1585
- types.jsxAttribute(
1586
- types.jsxIdentifier('initialData'),
1587
- types.jsxExpressionContainer(
1588
- types.conditionalExpression(
1589
- types.binaryExpression(
1590
- '===',
1591
- types.identifier(vars.pageStateVar),
1592
- types.numericLiteral(1)
1593
- ),
1594
- types.optionalMemberExpression(
1595
- types.identifier('props'),
1596
- types.identifier(vars.propsPrefix),
1597
- false,
1598
- true
1599
- ),
1600
- types.identifier('undefined')
1857
+ // Add initialData. See paginated+search updater for why we skip prefetch
1858
+ // reuse when a dynamic state-bound sort is active.
1859
+ if (!usage.dynamicSort) {
1860
+ dp.openingElement.attributes.push(
1861
+ types.jsxAttribute(
1862
+ types.jsxIdentifier('initialData'),
1863
+ types.jsxExpressionContainer(
1864
+ types.conditionalExpression(
1865
+ wrapInitialDataWithUrlFilterGuard(
1866
+ types.binaryExpression(
1867
+ '===',
1868
+ types.identifier(vars.pageStateVar),
1869
+ types.numericLiteral(1)
1870
+ ),
1871
+ usage
1872
+ ),
1873
+ types.optionalMemberExpression(
1874
+ types.identifier('props'),
1875
+ types.identifier(vars.propsPrefix),
1876
+ false,
1877
+ true
1878
+ ),
1879
+ types.identifier('undefined')
1880
+ )
1601
1881
  )
1602
1882
  )
1603
1883
  )
1604
- )
1884
+ }
1605
1885
 
1606
- // Add key
1886
+ // Add key — sort is intentionally NOT included; see paginated+search updater.
1607
1887
  dp.openingElement.attributes.push(
1608
1888
  types.jsxAttribute(
1609
1889
  types.jsxIdentifier('key'),
@@ -1664,71 +1944,20 @@ function updateDataProviderForSearchOnly(
1664
1944
  )
1665
1945
  }
1666
1946
 
1667
- // Add sorts if present
1668
- if (usage.sorts && usage.sorts.length > 0) {
1669
- paramsProps.push(
1670
- types.objectProperty(
1671
- types.identifier('sorts'),
1672
- types.callExpression(
1673
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1674
- [
1675
- types.arrayExpression(
1676
- usage.sorts.map((sort: any) =>
1677
- types.objectExpression([
1678
- types.objectProperty(
1679
- types.identifier('field'),
1680
- types.stringLiteral(sort.field || '')
1681
- ),
1682
- types.objectProperty(
1683
- types.identifier('order'),
1684
- types.stringLiteral(sort.order || '')
1685
- ),
1686
- ])
1687
- )
1688
- ),
1689
- ]
1690
- )
1691
- )
1692
- )
1693
- }
1947
+ // Add sorts if present (legacy static array wins; otherwise dynamic state-bound sort)
1948
+ appendSortsParam(paramsProps, usage.sorts, usage.dynamicSort)
1694
1949
 
1695
1950
  // Add filters if present
1696
- if (usage.filters && usage.filters.length > 0) {
1697
- paramsProps.push(
1698
- types.objectProperty(
1699
- types.identifier('filters'),
1700
- types.callExpression(
1701
- types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
1702
- [
1703
- types.arrayExpression(
1704
- usage.filters.map((filter: any) =>
1705
- types.objectExpression([
1706
- types.objectProperty(
1707
- types.identifier('source'),
1708
- types.stringLiteral(filter.source || '')
1709
- ),
1710
- types.objectProperty(
1711
- types.identifier('destination'),
1712
- ASTUtils.convertFilterDestinationToExpression(filter.destination)
1713
- ),
1714
- types.objectProperty(
1715
- types.identifier('operand'),
1716
- types.stringLiteral(filter.operand || '')
1717
- ),
1718
- ])
1719
- )
1720
- ),
1721
- ]
1722
- )
1723
- )
1724
- )
1725
- }
1951
+ appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
1726
1952
 
1727
- // Build useMemo dependencies including filter state IDs
1953
+ // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
1728
1954
  const memoDeps: types.Expression[] = [types.identifier(vars.debouncedSearchQueryVar)]
1729
- usage.filterStateIds.forEach((stateId) => {
1730
- memoDeps.push(types.identifier(stateId))
1731
- })
1955
+ const seenDeps = new Set<string>([vars.debouncedSearchQueryVar])
1956
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.filterStateIds)
1957
+ if (usage.dynamicSort) {
1958
+ pushStateIdsAsDeps(memoDeps, seenDeps, usage.dynamicSort.depStateIds)
1959
+ }
1960
+ pushUrlSearchParamMemoDeps(memoDeps, usage)
1732
1961
 
1733
1962
  dp.openingElement.attributes.push(
1734
1963
  types.jsxAttribute(
@@ -1742,26 +1971,32 @@ function updateDataProviderForSearchOnly(
1742
1971
  )
1743
1972
  )
1744
1973
 
1745
- // Add initialData
1746
- dp.openingElement.attributes.push(
1747
- types.jsxAttribute(
1748
- types.jsxIdentifier('initialData'),
1749
- types.jsxExpressionContainer(
1750
- types.conditionalExpression(
1751
- types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
1752
- types.optionalMemberExpression(
1753
- types.identifier('props'),
1754
- types.identifier(vars.propsPrefix),
1755
- false,
1756
- true
1757
- ),
1758
- types.identifier('undefined')
1974
+ // Add initialData. See paginated+search updater for why we skip prefetch
1975
+ // reuse when a dynamic state-bound sort is active.
1976
+ if (!usage.dynamicSort) {
1977
+ dp.openingElement.attributes.push(
1978
+ types.jsxAttribute(
1979
+ types.jsxIdentifier('initialData'),
1980
+ types.jsxExpressionContainer(
1981
+ types.conditionalExpression(
1982
+ wrapInitialDataWithUrlFilterGuard(
1983
+ types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
1984
+ usage
1985
+ ),
1986
+ types.optionalMemberExpression(
1987
+ types.identifier('props'),
1988
+ types.identifier(vars.propsPrefix),
1989
+ false,
1990
+ true
1991
+ ),
1992
+ types.identifier('undefined')
1993
+ )
1759
1994
  )
1760
1995
  )
1761
1996
  )
1762
- )
1997
+ }
1763
1998
 
1764
- // Add key
1999
+ // Add key — sort is intentionally NOT included; see paginated+search updater.
1765
2000
  dp.openingElement.attributes.push(
1766
2001
  types.jsxAttribute(
1767
2002
  types.jsxIdentifier('key'),
@@ -1849,9 +2084,8 @@ function updateDataProviderForPlain(dp: any, fileName: string, usage: DataSource
1849
2084
 
1850
2085
  // Build useMemo dependencies including filter state IDs
1851
2086
  const memoDeps: types.Expression[] = []
1852
- usage.filterStateIds.forEach((stateId) => {
1853
- memoDeps.push(types.identifier(stateId))
1854
- })
2087
+ pushStateIdsAsDeps(memoDeps, new Set<string>(), usage.filterStateIds)
2088
+ pushUrlSearchParamMemoDeps(memoDeps, usage)
1855
2089
 
1856
2090
  // Wrap params in useMemo with filter state dependencies
1857
2091
  const memoizedParams = types.callExpression(types.identifier('useMemo'), [
@@ -2348,7 +2582,8 @@ export default dataSourceModule.getCount
2348
2582
  function updateGetStaticProps(
2349
2583
  chunks: any[],
2350
2584
  registry: StateRegistry,
2351
- dependencies: Record<string, any>
2585
+ dependencies: Record<string, any>,
2586
+ folderPath?: string[]
2352
2587
  ): void {
2353
2588
  const getStaticPropsChunk = chunks.find((c) => c.name === 'getStaticProps')
2354
2589
  if (!getStaticPropsChunk || getStaticPropsChunk.type !== ChunkType.AST) {
@@ -2565,9 +2800,11 @@ function updateGetStaticProps(
2565
2800
 
2566
2801
  // Add import dependency for the fetcher
2567
2802
  if (!dependencies[fetcherImportName]) {
2803
+ const depth = (folderPath ? folderPath.length : 0) + 1
2804
+ const relativePrefix = '../'.repeat(depth)
2568
2805
  dependencies[fetcherImportName] = {
2569
2806
  type: 'local',
2570
- path: `../utils/data-sources/${fileName}`,
2807
+ path: `${relativePrefix}utils/data-sources/${fileName}`,
2571
2808
  }
2572
2809
  }
2573
2810
 
@@ -2781,9 +3018,11 @@ function updateGetStaticProps(
2781
3018
 
2782
3019
  // Add import dependency for the fetcher
2783
3020
  if (!dependencies[fetcherImportName]) {
3021
+ const depth = (folderPath ? folderPath.length : 0) + 1
3022
+ const relativePrefix = '../'.repeat(depth)
2784
3023
  dependencies[fetcherImportName] = {
2785
3024
  type: 'local',
2786
- path: `../utils/data-sources/${fileName}`,
3025
+ path: `${relativePrefix}utils/data-sources/${fileName}`,
2787
3026
  }
2788
3027
  }
2789
3028
  }