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

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 (199) hide show
  1. package/__tests__/_helpers/first-page-runtime.ts +184 -0
  2. package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
  3. package/__tests__/booking-item-transform.test.ts +120 -0
  4. package/__tests__/browser-row-policy.test.ts +210 -4
  5. package/__tests__/data-source-request-locale.test.ts +4 -3
  6. package/__tests__/fetchers.test.ts +7 -3
  7. package/__tests__/fixed-list-sort.test.ts +1 -1
  8. package/__tests__/infinite-scroll.test.ts +363 -9
  9. package/__tests__/list-sort-order.test.ts +61 -0
  10. package/__tests__/null-filter-where.test.ts +132 -0
  11. package/__tests__/page-data-visitor-read.test.ts +196 -0
  12. package/__tests__/pg-date-parser.test.ts +73 -0
  13. package/__tests__/plain-list-sort.test.ts +2 -2
  14. package/__tests__/plain-list-starting-sort.test.ts +323 -0
  15. package/__tests__/product-price-sort.ts +11 -6
  16. package/__tests__/range-control.test.ts +159 -0
  17. package/__tests__/refetch-loading-state.test.ts +158 -2
  18. package/__tests__/search-input-marker.test.ts +86 -0
  19. package/__tests__/search-url-sync.test.ts +34 -8
  20. package/__tests__/starting-sort-first-page.test.ts +285 -0
  21. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  22. package/dist/cjs/cache/ast.d.ts +2 -0
  23. package/dist/cjs/cache/ast.d.ts.map +1 -1
  24. package/dist/cjs/cache/ast.js +5 -2
  25. package/dist/cjs/cache/ast.js.map +1 -1
  26. package/dist/cjs/count-effect.d.ts +7 -1
  27. package/dist/cjs/count-effect.d.ts.map +1 -1
  28. package/dist/cjs/count-effect.js +15 -5
  29. package/dist/cjs/count-effect.js.map +1 -1
  30. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  31. package/dist/cjs/data-source-fetchers.js +10 -2
  32. package/dist/cjs/data-source-fetchers.js.map +1 -1
  33. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  34. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  35. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  36. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +3 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/raw-query.js +2 -1
  42. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  43. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/teleport.js +4 -2
  45. package/dist/cjs/fetchers/teleport.js.map +1 -1
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +14 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/infinite-scroll.d.ts +89 -4
  53. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  54. package/dist/cjs/infinite-scroll.js +243 -33
  55. package/dist/cjs/infinite-scroll.js.map +1 -1
  56. package/dist/cjs/list-sort-order.d.ts +14 -0
  57. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  58. package/dist/cjs/list-sort-order.js +36 -0
  59. package/dist/cjs/list-sort-order.js.map +1 -0
  60. package/dist/cjs/loading-state.d.ts +23 -2
  61. package/dist/cjs/loading-state.d.ts.map +1 -1
  62. package/dist/cjs/loading-state.js +50 -15
  63. package/dist/cjs/loading-state.js.map +1 -1
  64. package/dist/cjs/pagination-controls.d.ts +22 -3
  65. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  66. package/dist/cjs/pagination-controls.js +29 -3
  67. package/dist/cjs/pagination-controls.js.map +1 -1
  68. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  69. package/dist/cjs/pagination-plugin.js +336 -164
  70. package/dist/cjs/pagination-plugin.js.map +1 -1
  71. package/dist/cjs/range-control.d.ts +45 -0
  72. package/dist/cjs/range-control.d.ts.map +1 -0
  73. package/dist/cjs/range-control.js +165 -0
  74. package/dist/cjs/range-control.js.map +1 -0
  75. package/dist/cjs/row-order.d.ts +3 -2
  76. package/dist/cjs/row-order.d.ts.map +1 -1
  77. package/dist/cjs/row-order.js +5 -4
  78. package/dist/cjs/row-order.js.map +1 -1
  79. package/dist/cjs/search-input.d.ts +42 -0
  80. package/dist/cjs/search-input.d.ts.map +1 -0
  81. package/dist/cjs/search-input.js +158 -0
  82. package/dist/cjs/search-input.js.map +1 -0
  83. package/dist/cjs/sort-utils.d.ts +53 -1
  84. package/dist/cjs/sort-utils.d.ts.map +1 -1
  85. package/dist/cjs/sort-utils.js +105 -13
  86. package/dist/cjs/sort-utils.js.map +1 -1
  87. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  88. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  89. package/dist/cjs/transformations/booking-item.js +27 -0
  90. package/dist/cjs/transformations/booking-item.js.map +1 -0
  91. package/dist/cjs/transformations/index.d.ts +4 -2
  92. package/dist/cjs/transformations/index.d.ts.map +1 -1
  93. package/dist/cjs/transformations/index.js +14 -5
  94. package/dist/cjs/transformations/index.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts +3 -2
  97. package/dist/cjs/utils.d.ts.map +1 -1
  98. package/dist/cjs/utils.js +48 -18
  99. package/dist/cjs/utils.js.map +1 -1
  100. package/dist/esm/cache/ast.d.ts +2 -0
  101. package/dist/esm/cache/ast.d.ts.map +1 -1
  102. package/dist/esm/cache/ast.js +3 -1
  103. package/dist/esm/cache/ast.js.map +1 -1
  104. package/dist/esm/count-effect.d.ts +7 -1
  105. package/dist/esm/count-effect.d.ts.map +1 -1
  106. package/dist/esm/count-effect.js +15 -5
  107. package/dist/esm/count-effect.js.map +1 -1
  108. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  109. package/dist/esm/data-source-fetchers.js +10 -2
  110. package/dist/esm/data-source-fetchers.js.map +1 -1
  111. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  112. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  113. package/dist/esm/fetchers/pg-client-import.js +12 -0
  114. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  115. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  116. package/dist/esm/fetchers/postgresql.js +3 -1
  117. package/dist/esm/fetchers/postgresql.js.map +1 -1
  118. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  119. package/dist/esm/fetchers/raw-query.js +2 -1
  120. package/dist/esm/fetchers/raw-query.js.map +1 -1
  121. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  122. package/dist/esm/fetchers/teleport.js +4 -2
  123. package/dist/esm/fetchers/teleport.js.map +1 -1
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  127. package/dist/esm/index.d.ts.map +1 -1
  128. package/dist/esm/index.js +14 -2
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/infinite-scroll.d.ts +89 -4
  131. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  132. package/dist/esm/infinite-scroll.js +237 -32
  133. package/dist/esm/infinite-scroll.js.map +1 -1
  134. package/dist/esm/list-sort-order.d.ts +14 -0
  135. package/dist/esm/list-sort-order.d.ts.map +1 -0
  136. package/dist/esm/list-sort-order.js +32 -0
  137. package/dist/esm/list-sort-order.js.map +1 -0
  138. package/dist/esm/loading-state.d.ts +23 -2
  139. package/dist/esm/loading-state.d.ts.map +1 -1
  140. package/dist/esm/loading-state.js +50 -15
  141. package/dist/esm/loading-state.js.map +1 -1
  142. package/dist/esm/pagination-controls.d.ts +22 -3
  143. package/dist/esm/pagination-controls.d.ts.map +1 -1
  144. package/dist/esm/pagination-controls.js +28 -2
  145. package/dist/esm/pagination-controls.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +340 -168
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/range-control.d.ts +45 -0
  150. package/dist/esm/range-control.d.ts.map +1 -0
  151. package/dist/esm/range-control.js +137 -0
  152. package/dist/esm/range-control.js.map +1 -0
  153. package/dist/esm/row-order.d.ts +3 -2
  154. package/dist/esm/row-order.d.ts.map +1 -1
  155. package/dist/esm/row-order.js +5 -4
  156. package/dist/esm/row-order.js.map +1 -1
  157. package/dist/esm/search-input.d.ts +42 -0
  158. package/dist/esm/search-input.d.ts.map +1 -0
  159. package/dist/esm/search-input.js +129 -0
  160. package/dist/esm/search-input.js.map +1 -0
  161. package/dist/esm/sort-utils.d.ts +53 -1
  162. package/dist/esm/sort-utils.d.ts.map +1 -1
  163. package/dist/esm/sort-utils.js +100 -12
  164. package/dist/esm/sort-utils.js.map +1 -1
  165. package/dist/esm/transformations/booking-item.d.ts +24 -0
  166. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  167. package/dist/esm/transformations/booking-item.js +23 -0
  168. package/dist/esm/transformations/booking-item.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +4 -2
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +14 -5
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  174. package/dist/esm/utils.d.ts +3 -2
  175. package/dist/esm/utils.d.ts.map +1 -1
  176. package/dist/esm/utils.js +49 -19
  177. package/dist/esm/utils.js.map +1 -1
  178. package/package.json +5 -5
  179. package/src/cache/ast.ts +4 -1
  180. package/src/count-effect.ts +31 -8
  181. package/src/data-source-fetchers.ts +33 -14
  182. package/src/fetchers/pg-client-import.ts +16 -0
  183. package/src/fetchers/postgresql.ts +15 -9
  184. package/src/fetchers/raw-query.ts +2 -1
  185. package/src/fetchers/teleport.ts +19 -10
  186. package/src/fetchers/utils/browser-row-policy.ts +79 -5
  187. package/src/index.ts +8 -2
  188. package/src/infinite-scroll.ts +420 -81
  189. package/src/list-sort-order.ts +34 -0
  190. package/src/loading-state.ts +85 -13
  191. package/src/pagination-controls.ts +26 -3
  192. package/src/pagination-plugin.ts +532 -218
  193. package/src/range-control.ts +207 -0
  194. package/src/row-order.ts +5 -4
  195. package/src/search-input.ts +190 -0
  196. package/src/sort-utils.ts +164 -21
  197. package/src/transformations/booking-item.ts +71 -0
  198. package/src/transformations/index.ts +25 -2
  199. package/src/utils.ts +55 -18
@@ -0,0 +1,159 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { ComponentStructure } from '@teleporthq/teleport-types'
4
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
5
+ import {
6
+ makeUidlNode,
7
+ makeWidgetComponentChunk,
8
+ RANGE_DRAWN_TEXT,
9
+ type RepeaterOptions,
10
+ } from './_helpers/pagination-widget-fixture'
11
+
12
+ type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
13
+
14
+ const runPlugin = async (repeater: RepeaterOptions, controls: ControlOptions): Promise<string> => {
15
+ const chunk = makeWidgetComponentChunk(controls)
16
+ const structure: ComponentStructure = {
17
+ uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
18
+ chunks: [chunk],
19
+ dependencies: {},
20
+ options: { dataSources: {}, extractedResources: {} },
21
+ } as never
22
+ await createNextArrayMapperPaginationPlugin()(structure)
23
+ return generator(chunk.content as types.Node).code
24
+ }
25
+
26
+ const collapse = (code: string): string => code.replace(/\s+/g, ' ')
27
+
28
+ const TOTAL_STATE = 'const [ds_0_total, setDs_0_total] = useState(null)'
29
+ const DRAWN = `ds_0_total === null ? "${RANGE_DRAWN_TEXT}" :`
30
+ const FROM_BY_PAGE = '(ds_0_total === 0 ? 0 : (ds_0_page - 1) * 20 + 1).toLocaleString("en-US")'
31
+ const FROM_FIRST_ROW = '(ds_0_total === 0 ? 0 : 1).toLocaleString("en-US")'
32
+ const TO = 'Math.min(ds_0_page * 20, ds_0_total).toLocaleString("en-US")'
33
+ const TOTAL = 'ds_0_total.toLocaleString("en-US")'
34
+
35
+ describe('pagination plugin — range control', () => {
36
+ it('fills the authored words with the page window in buttons mode', async () => {
37
+ const flat = collapse(await runPlugin({}, { controls: ['previous', 'range', 'next'] }))
38
+
39
+ // The total is a state of its own — `maxPages` cannot give it back — and
40
+ // the count effect fills both from the same response.
41
+ expect(flat).toContain(TOTAL_STATE)
42
+ expect(flat).toContain('setDs_0_maxPages(')
43
+ expect(flat).toContain('setDs_0_total(data.count)')
44
+
45
+ // The drawn text stays until the count has answered; then the template.
46
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
47
+ // The marker and template ride along, so the DOM carries the contract.
48
+ expect(flat).toContain('data-tq-pagination-control="range"')
49
+ expect(flat).toContain('data-tq-range-template=')
50
+ })
51
+
52
+ it('counts from the first row in an appending list and survives the mode pruning', async () => {
53
+ const flat = collapse(
54
+ await runPlugin(
55
+ { infiniteScroll: true, infiniteScrollLoadMore: true },
56
+ { controls: ['previous', 'range', 'load-more', 'next'] }
57
+ )
58
+ )
59
+
60
+ // Previous/Next are pruned for an appending list; the range is not.
61
+ expect(flat).not.toContain('thq-previous-elm')
62
+ expect(flat).not.toContain('thq-next-elm')
63
+ expect(flat).toContain('thq-load-more-elm')
64
+ expect(flat).toContain('data-tq-pagination-control="range"')
65
+
66
+ // Everything loaded so far: 1 .. min(pages × perPage, total).
67
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_FIRST_ROW}}–\${${TO}} of \${${TOTAL}}\``)
68
+ expect(flat).not.toContain(FROM_BY_PAGE)
69
+ })
70
+
71
+ it('is kept and wired beside a sentinel-driven infinite scroll too', async () => {
72
+ const flat = collapse(await runPlugin({ infiniteScroll: true }, { controls: ['range'] }))
73
+
74
+ expect(flat).toContain('ds_0_sentinelRef')
75
+ expect(flat).toContain(TOTAL_STATE)
76
+ expect(flat).toContain(FROM_FIRST_ROW)
77
+ })
78
+
79
+ it('survives numbered mode with the page window', async () => {
80
+ const flat = collapse(
81
+ await runPlugin(
82
+ { paginationMode: 'numbered' },
83
+ { controls: ['first', 'previous', 'pages', 'range', 'next', 'last'] }
84
+ )
85
+ )
86
+
87
+ expect(flat).toContain('ds_0_pageTokens.map(token =>')
88
+ expect(flat).toContain('data-tq-pagination-control="range"')
89
+ expect(flat).toContain(FROM_BY_PAGE)
90
+ })
91
+
92
+ it('reads the page off the combined state beside a search input', async () => {
93
+ const flat = collapse(
94
+ await runPlugin({ searchEnabled: true }, { controls: ['previous', 'range', 'next'] })
95
+ )
96
+
97
+ expect(flat).toContain(TOTAL_STATE)
98
+ expect(flat).toContain('setDs_0_total(data.count)')
99
+ expect(flat).toContain(
100
+ '(ds_0_total === 0 ? 0 : (ds_0_state.page - 1) * 20 + 1).toLocaleString("en-US")'
101
+ )
102
+ expect(flat).toContain('Math.min(ds_0_state.page * 20, ds_0_total).toLocaleString("en-US")')
103
+ })
104
+
105
+ it('reads a template the JSX printer escaped the braces of', async () => {
106
+ const flat = collapse(
107
+ await runPlugin(
108
+ {},
109
+ {
110
+ controls: ['range'],
111
+ rangeTemplate: 'Showing &#123;from&#125;–&#123;to&#125; of &#123;total&#125;',
112
+ }
113
+ )
114
+ )
115
+ expect(flat).not.toContain('`Showing &#123;')
116
+ expect(flat).toMatch(/`Showing \$\{[^`]*–\$\{[^`]*of \$\{[^`]*`/)
117
+ })
118
+
119
+ it('falls back to "{from}–{to} of {total}" when no template is authored', async () => {
120
+ const flat = collapse(await runPlugin({}, { controls: ['range'], rangeTemplate: null }))
121
+
122
+ expect(flat).not.toContain('data-tq-range-template')
123
+ expect(flat).toContain(`${DRAWN} \`\${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
124
+ })
125
+
126
+ it('wires a range outside every widget to the mapper it names, wherever it sits', async () => {
127
+ // Placed BEFORE the provider — a toolbar above the list — so the
128
+ // container-ownership rule could never have reached it.
129
+ const flat = collapse(
130
+ await runPlugin(
131
+ { nodeId: 'mapper-1' },
132
+ { controls: ['previous', 'next'], standaloneRange: { forNodeId: 'mapper-1' } }
133
+ )
134
+ )
135
+
136
+ expect(flat).toContain('data-tq-pagination-for="mapper-1"')
137
+ expect(flat).toContain(TOTAL_STATE)
138
+ expect(flat).toContain(`${DRAWN} \`Showing \${${FROM_BY_PAGE}}–\${${TO}} of \${${TOTAL}}\``)
139
+ })
140
+
141
+ it('leaves a range naming a mapper this component does not render as drawn', async () => {
142
+ const flat = collapse(
143
+ await runPlugin(
144
+ { nodeId: 'mapper-1' },
145
+ { controls: ['previous', 'next'], standaloneRange: { forNodeId: 'someone-else' } }
146
+ )
147
+ )
148
+
149
+ expect(flat).toContain(`>${RANGE_DRAWN_TEXT}</div>`)
150
+ expect(flat).not.toContain('ds_0_total === null')
151
+ })
152
+
153
+ it('changes nothing for a component without a range control', async () => {
154
+ const flat = collapse(await runPlugin({}, { controls: ['previous', 'next'] }))
155
+
156
+ expect(flat).not.toContain('ds_0_total')
157
+ expect(flat).not.toContain('setDs_0_total')
158
+ })
159
+ })
@@ -98,7 +98,8 @@ const makeComponentChunk = (dataProvider: types.JSXElement): ChunkDefinition =>
98
98
  // A `data-source-list > cms-list-repeater` UIDL. `category` decides which
99
99
  // DataProvider updater runs, so every one of them is covered below.
100
100
  const makeUidlNode = (
101
- category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
101
+ category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain',
102
+ repeater: Record<string, unknown> = {}
102
103
  // tslint:disable-next-line:no-any
103
104
  ): any => ({
104
105
  type: 'data-source-list',
@@ -120,6 +121,7 @@ const makeUidlNode = (
120
121
  searchEnabled: category === 'paginated+search' || category === 'search-only',
121
122
  searchDebounce: 300,
122
123
  nodes: { list: { type: 'element', content: { elementType: 'div' } } },
124
+ ...repeater,
123
125
  },
124
126
  },
125
127
  },
@@ -129,11 +131,17 @@ const makeUidlNode = (
129
131
  const runPlugin = async (options: {
130
132
  category?: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
131
133
  withLoadingSlot?: boolean
134
+ repeater?: Record<string, unknown>
135
+ onChunk?: (chunk: ChunkDefinition) => void
132
136
  }): Promise<string> => {
133
137
  const dataProvider = makeDataProviderJSX({ withLoadingSlot: options.withLoadingSlot !== false })
134
138
  const chunk = makeComponentChunk(dataProvider)
139
+ options.onChunk?.(chunk)
135
140
  const structure: ComponentStructure = {
136
- uidl: { name: 'ProductsList', node: makeUidlNode(options.category || 'paginated+search') },
141
+ uidl: {
142
+ name: 'ProductsList',
143
+ node: makeUidlNode(options.category || 'paginated+search', options.repeater),
144
+ },
137
145
  chunks: [chunk],
138
146
  dependencies: {},
139
147
  options: { dataSources: {}, extractedResources: {} },
@@ -285,3 +293,151 @@ describe('pagination plugin — refetch loading state', () => {
285
293
  expect(code).toContain('persistDataDuringLoading={true}')
286
294
  })
287
295
  })
296
+
297
+ /**
298
+ * An appending list (infinite scroll, Load more) loads its next page BELOW the
299
+ * rows on show. Raising the flag for that request too swapped every row for the
300
+ * loading slot until the page arrived: the list collapsed under the visitor's
301
+ * scroll on each append. Only a list loading afresh shows its loading slot.
302
+ */
303
+ describe('pagination plugin — refetch loading state of an appending list', () => {
304
+ it.each([
305
+ ['infinite scroll', { infiniteScroll: true }],
306
+ ['Load more', { infiniteScroll: true, infiniteScrollLoadMore: true }],
307
+ ])(
308
+ 'keeps the rows on screen while the next page of a list with %s loads',
309
+ async (_, repeater) => {
310
+ const code = (await runPlugin({ category: 'paginated-only', repeater })).replace(/\s+/g, ' ')
311
+
312
+ expect(code).toContain('const [ds_0_isAppending, setDs_0_isAppending] = useState(false)')
313
+ // Recorded with the request, from the request the list now stands on.
314
+ expect(code).toContain(
315
+ 'setDs_0_isAppending(ds_0_accumRef.current.latest.page > 1 && ds_0_accumRef.current.latest.sig === ds_0_accumRef.current.sig); setDs_0_isFetching(true);'
316
+ )
317
+ expect(code.indexOf('ds_0_accumRef.current.latest = {')).toBeLessThan(
318
+ code.indexOf('setDs_0_isAppending(ds_0_accumRef')
319
+ )
320
+ expect(code).toContain('persistDataDuringLoading={!ds_0_isFetching || ds_0_isAppending}')
321
+ }
322
+ )
323
+
324
+ it('leaves a list that pages unchanged: a page change loads it afresh', async () => {
325
+ const code = await runPlugin({ category: 'paginated-only' })
326
+
327
+ expect(code).toContain('persistDataDuringLoading={!ds_0_isFetching}')
328
+ expect(code).not.toContain('isAppending')
329
+ })
330
+ })
331
+
332
+ /**
333
+ * Executed: the `fetchData` and `persistDataDuringLoading` the page emits, under
334
+ * a model of what `DataProvider` (@teleporthq/react-components) paints:
335
+ *
336
+ * on a params change: status = "loading"; if (!persistDataDuringLoading) data = undefined
337
+ * while loading: persistDataDuringLoading && data ? renderSuccess(data) : renderLoading()
338
+ * once answered: data = <what fetchData resolved with>
339
+ */
340
+ describe('an appending list, run', () => {
341
+ const PER_PAGE = 20
342
+ // tslint:disable:no-any
343
+
344
+ const findAttribute = (root: types.Node, name: string): types.Expression => {
345
+ let found: types.Expression | undefined
346
+ types.traverseFast(root, (node) => {
347
+ if (
348
+ !found &&
349
+ node.type === 'JSXAttribute' &&
350
+ node.name.name === name &&
351
+ node.value?.type === 'JSXExpressionContainer'
352
+ ) {
353
+ found = node.value.expression as types.Expression
354
+ }
355
+ })
356
+ return found!
357
+ }
358
+
359
+ const bootList = async () => {
360
+ let chunk: ChunkDefinition | undefined
361
+ await runPlugin({
362
+ category: 'paginated-only',
363
+ repeater: { infiniteScroll: true, infiniteScrollLoadMore: true },
364
+ onChunk: (made) => (chunk = made),
365
+ })
366
+ const content = chunk!.content as types.Node
367
+ const memoized = findAttribute(content, 'fetchData') as types.CallExpression
368
+ const persistCode = generator(findAttribute(content, 'persistDataDuringLoading')).code
369
+
370
+ const state = { isFetching: false, isAppending: false }
371
+ const scope = {
372
+ ds_0_accumRef: { current: { sig: '', pages: {}, latest: null } },
373
+ ds_0_fetchSeq: { current: 0 },
374
+ ds_0_fetchesInFlight: { current: 0 },
375
+ ds_0_latestData: { current: undefined },
376
+ setDs_0_hasMoreFlag: () => undefined,
377
+ setDs_0_isFetching: (value: boolean) => (state.isFetching = value),
378
+ setDs_0_isAppending: (value: boolean) => (state.isAppending = value),
379
+ }
380
+ const answers: Array<() => void> = []
381
+ const fakeFetch = (url: string) => {
382
+ const query = new URL(url, 'http://shop.test').searchParams
383
+ const page = Number(query.get('page'))
384
+ const rows = Array.from({ length: PER_PAGE }, (_, i) => `${query.get('sorts')}${page}-${i}`)
385
+ return new Promise((resolve) => answers.push(() => resolve({ json: () => ({ data: rows }) })))
386
+ }
387
+ const fetchData = new Function(
388
+ 'fetch',
389
+ ...Object.keys(scope),
390
+ `return (${generator(memoized.arguments[0]).code})`
391
+ )(fakeFetch, ...Object.values(scope))
392
+ const persistDataDuringLoading = (): boolean =>
393
+ new Function('ds_0_isFetching', 'ds_0_isAppending', `return (${persistCode})`)(
394
+ state.isFetching,
395
+ state.isAppending
396
+ )
397
+
398
+ // The provider: a component's list, so nothing on show until page 1.
399
+ let data: string[] | undefined
400
+ let status = 'idle'
401
+ let persistAtLastPaint = persistDataDuringLoading()
402
+ const paint = () => {
403
+ persistAtLastPaint = persistDataDuringLoading()
404
+ if (status === 'success') {
405
+ return `${data!.length} rows`
406
+ }
407
+ return persistAtLastPaint && data ? `${data.length} rows` : 'loading slot'
408
+ }
409
+ const load = async (page: number, sorts: string) => {
410
+ status = 'loading'
411
+ if (!persistAtLastPaint) {
412
+ data = undefined
413
+ }
414
+ const answered = fetchData({ page, perPage: PER_PAGE, sorts })
415
+ const whileLoading = paint()
416
+ answers.shift()!()
417
+ data = await answered
418
+ status = 'success'
419
+ return { whileLoading, after: paint() }
420
+ }
421
+ return { load }
422
+ }
423
+
424
+ it('paints its loading slot for its first page, then keeps the rows through every append', async () => {
425
+ const list = await bootList()
426
+
427
+ expect(await list.load(1, 'A')).toEqual({ whileLoading: 'loading slot', after: '20 rows' })
428
+ expect(await list.load(2, 'A')).toEqual({ whileLoading: '20 rows', after: '40 rows' })
429
+ expect(await list.load(3, 'A')).toEqual({ whileLoading: '40 rows', after: '60 rows' })
430
+ })
431
+
432
+ it('paints the loading slot again for a fresh list: a sort, filter or search change', async () => {
433
+ const list = await bootList()
434
+ await list.load(1, 'A')
435
+ await list.load(2, 'A')
436
+
437
+ // The page the change fires before the list is reset to page 1 is another
438
+ // query's, and no append either.
439
+ expect((await list.load(2, 'B')).whileLoading).toBe('loading slot')
440
+ expect(await list.load(1, 'B')).toEqual({ whileLoading: 'loading slot', after: '20 rows' })
441
+ expect(await list.load(2, 'B')).toEqual({ whileLoading: '20 rows', after: '40 rows' })
442
+ })
443
+ })
@@ -0,0 +1,86 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import { ComponentStructure } from '@teleporthq/teleport-types'
4
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
5
+ import {
6
+ makeMultiMapperUidl,
7
+ makeWidgetComponentChunk,
8
+ type RepeaterOptions,
9
+ } from './_helpers/pagination-widget-fixture'
10
+
11
+ type ControlOptions = Parameters<typeof makeWidgetComponentChunk>[0]
12
+
13
+ /**
14
+ * A page's OWN search box, marked `data-tq-search-for` with its mapper's node
15
+ * id, is wired like the platform's `search-input`: controlled by the mapper's
16
+ * query state. It is paired by the id it names, never by its place among the
17
+ * platform inputs.
18
+ */
19
+
20
+ const runPlugin = async (
21
+ repeaters: RepeaterOptions[],
22
+ controls: ControlOptions
23
+ ): Promise<string> => {
24
+ const chunk = makeWidgetComponentChunk(controls)
25
+ const structure: ComponentStructure = {
26
+ uidl: { name: 'TestComponent', node: makeMultiMapperUidl(repeaters) },
27
+ chunks: [chunk],
28
+ dependencies: {},
29
+ options: { dataSources: {}, extractedResources: {} },
30
+ } as never
31
+ await createNextArrayMapperPaginationPlugin()(structure)
32
+ return generator(chunk.content as types.Node).code
33
+ }
34
+
35
+ const collapse = (code: string): string => code.replace(/\s+/g, ' ')
36
+
37
+ const wiredTo = (index: number): string =>
38
+ `onChange={e => setDs_${index}_searchQuery(e.target.value)} value={ds_${index}_searchQuery}`
39
+
40
+ describe('pagination plugin — a page’s own search box', () => {
41
+ it('controls a marked input by the query of the mapper it names, and keeps Enter from submitting', async () => {
42
+ const flat = collapse(
43
+ await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
44
+ controls: ['previous', 'next'],
45
+ standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_orders' }],
46
+ })
47
+ )
48
+ expect(flat).toContain(`name="hm-search" data-tq-search-for="TQ_orders" ${wiredTo(0)}`)
49
+ expect(flat).toContain('onKeyDown={e => { if (e.key === "Enter") e.preventDefault(); }}')
50
+ })
51
+
52
+ it('pairs by the id named, whatever the order: the platform input keeps its place', async () => {
53
+ const flat = collapse(
54
+ await runPlugin(
55
+ [
56
+ { searchEnabled: true, nodeId: 'TQ_jobs' },
57
+ { searchEnabled: true, nodeId: 'TQ_orders' },
58
+ ],
59
+ {
60
+ controls: ['previous', 'next'],
61
+ mappers: 2,
62
+ standaloneSearch: [
63
+ { name: 'hm-search', forNodeId: 'TQ_orders' },
64
+ { name: 'platform-search' },
65
+ ],
66
+ }
67
+ )
68
+ )
69
+ // The marked box searches the second mapper; the platform box, first of its kind, the first.
70
+ expect(flat).toContain(`data-tq-search-for="TQ_orders" ${wiredTo(1)}`)
71
+ expect(flat).toContain(
72
+ `name="platform-search" className="products-list-search-input" ${wiredTo(0)}`
73
+ )
74
+ })
75
+
76
+ it('leaves a marked input alone when no search-enabled mapper carries its id', async () => {
77
+ const flat = collapse(
78
+ await runPlugin([{ searchEnabled: true, nodeId: 'TQ_orders' }], {
79
+ controls: ['previous', 'next'],
80
+ standaloneSearch: [{ name: 'hm-search', forNodeId: 'TQ_elsewhere' }],
81
+ })
82
+ )
83
+ expect(flat).toContain('name="hm-search" data-tq-search-for="TQ_elsewhere" />')
84
+ expect(flat).not.toContain('setDs_0_searchQuery(e.target.value)')
85
+ })
86
+ })
@@ -40,7 +40,7 @@ const makeComponentChunk = (): ChunkDefinition => {
40
40
  // A `data-source-list > cms-list-repeater` UIDL: a paginated + search-enabled
41
41
  // products list, optionally bound to a URL search-param key.
42
42
  // tslint:disable-next-line:no-any
43
- const makeUidlNode = (searchUrlParamKey?: string): any => ({
43
+ const makeUidlNode = (searchUrlParamKey?: string, pageUrlParamKey?: string): any => ({
44
44
  type: 'data-source-list',
45
45
  content: {
46
46
  renderPropIdentifier: 'items',
@@ -60,6 +60,7 @@ const makeUidlNode = (searchUrlParamKey?: string): any => ({
60
60
  searchEnabled: true,
61
61
  searchDebounce: 300,
62
62
  ...(searchUrlParamKey ? { searchUrlParamKey } : {}),
63
+ ...(pageUrlParamKey ? { pageUrlParamKey } : {}),
63
64
  nodes: { list: { type: 'element', content: { elementType: 'div' } } },
64
65
  },
65
66
  },
@@ -67,10 +68,17 @@ const makeUidlNode = (searchUrlParamKey?: string): any => ({
67
68
  },
68
69
  })
69
70
 
70
- const runPlugin = async (searchUrlParamKey?: string, paginated = true): Promise<string> => {
71
+ interface RunOptions {
72
+ paginated?: boolean
73
+ pageUrlParamKey?: string
74
+ infiniteScroll?: boolean
75
+ }
76
+
77
+ const runPlugin = async (searchUrlParamKey?: string, options: RunOptions = {}): Promise<string> => {
71
78
  const chunk = makeComponentChunk()
72
- const node = makeUidlNode(searchUrlParamKey)
73
- node.content.nodes.success.content.paginated = paginated
79
+ const node = makeUidlNode(searchUrlParamKey, options.pageUrlParamKey)
80
+ node.content.nodes.success.content.paginated = options.paginated ?? true
81
+ node.content.nodes.success.content.infiniteScroll = !!options.infiniteScroll
74
82
  const structure: ComponentStructure = {
75
83
  uidl: { name: 'TestComponent', node },
76
84
  chunks: [chunk],
@@ -108,19 +116,37 @@ describe('pagination plugin — search input URL two-way sync (searchUrlParamKey
108
116
  )
109
117
  expect(code).toContain('}, [ds_0_state.debouncedQuery, router.isReady])')
110
118
 
111
- // Read-back (URL → input AND applied query at once, keeping the page),
112
- // functional setState bail-out (loop-free), then the handshake flag.
119
+ // Read-back (URL → input AND applied query at once, from page 1: the page
120
+ // is not in the address), functional setState bail-out (loop-free), then
121
+ // the handshake flag.
113
122
  expect(code).toContain('const __urlValue = router.query.searchKeyword')
114
123
  expect(code).toContain('ds_0_adoptUrlSearch(prev => prev === __nextValue ? prev : __nextValue)')
115
124
  expect(code).toContain('ds_0_searchUrlAdopted.current = true')
116
125
  expect(code).toContain('}, [router.query.searchKeyword, router.isReady])')
117
126
  expect(code.replace(/\s+/g, ' ')).toContain(
118
- 'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_state(state => { const next = update(state.debouncedQuery); return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next }; }); };'
127
+ 'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_state(state => { const next = update(state.debouncedQuery); return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next }; }); };'
128
+ )
129
+ })
130
+
131
+ it('starts an appending list on page 1 when the address brings a query', async () => {
132
+ // The scroll sentinel has usually fetched page 2 before the router is
133
+ // ready: kept, the query's first answer was its page 2 — empty when only a
134
+ // few rows match.
135
+ const code = await runPlugin('q', { infiniteScroll: true })
136
+ expect(code.replace(/\s+/g, ' ')).toContain(
137
+ 'return next === state.debouncedQuery ? state : { ...state, page: 1, debouncedQuery: next };'
138
+ )
139
+ })
140
+
141
+ it('keeps the page of a list whose page is in the address too', async () => {
142
+ const code = await runPlugin('searchKeyword', { pageUrlParamKey: 'page' })
143
+ expect(code.replace(/\s+/g, ' ')).toContain(
144
+ 'return next === state.debouncedQuery ? state : { ...state, debouncedQuery: next };'
119
145
  )
120
146
  })
121
147
 
122
148
  it('adopts the address into both queries of a list without pages too', async () => {
123
- const code = await runPlugin('searchKeyword', false)
149
+ const code = await runPlugin('searchKeyword', { paginated: false })
124
150
  expect(code).not.toContain('window.location.search')
125
151
  expect(code.replace(/\s+/g, ' ')).toContain(
126
152
  'const ds_0_adoptUrlSearch = update => { setDs_0_searchQuery(update); setDs_0_debouncedQuery(update); };'