@teleporthq/teleport-plugin-next-data-source 0.44.0 → 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.
- package/__tests__/_helpers/first-page-runtime.ts +184 -0
- package/__tests__/_helpers/pagination-widget-fixture.ts +67 -1
- package/__tests__/browser-row-policy.test.ts +202 -2
- package/__tests__/data-source-request-locale.test.ts +4 -3
- package/__tests__/fetchers.test.ts +6 -2
- package/__tests__/infinite-scroll.test.ts +210 -5
- package/__tests__/page-data-visitor-read.test.ts +196 -0
- package/__tests__/plain-list-sort.test.ts +1 -1
- package/__tests__/plain-list-starting-sort.test.ts +323 -0
- package/__tests__/refetch-loading-state.test.ts +158 -2
- package/__tests__/starting-sort-first-page.test.ts +285 -0
- package/dist/cjs/cache/ast.d.ts +2 -0
- package/dist/cjs/cache/ast.d.ts.map +1 -1
- package/dist/cjs/cache/ast.js +5 -2
- package/dist/cjs/cache/ast.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +10 -2
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +1 -1
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +64 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +102 -3
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/loading-state.d.ts +23 -2
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +50 -15
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +200 -64
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/sort-utils.d.ts +53 -1
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +105 -13
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +3 -2
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +48 -18
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/cache/ast.d.ts +2 -0
- package/dist/esm/cache/ast.d.ts.map +1 -1
- package/dist/esm/cache/ast.js +3 -1
- package/dist/esm/cache/ast.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +10 -2
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +1 -1
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +64 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +97 -2
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/loading-state.d.ts +23 -2
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +50 -15
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +204 -68
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/sort-utils.d.ts +53 -1
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +100 -12
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +3 -2
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +49 -19
- package/dist/esm/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/cache/ast.ts +4 -1
- package/src/data-source-fetchers.ts +33 -14
- package/src/fetchers/teleport.ts +8 -2
- package/src/fetchers/utils/browser-row-policy.ts +79 -5
- package/src/index.ts +8 -2
- package/src/infinite-scroll.ts +182 -4
- package/src/loading-state.ts +85 -13
- package/src/pagination-plugin.ts +369 -83
- package/src/sort-utils.ts +164 -21
- package/src/utils.ts +55 -18
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor no-any */
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import generator from '@babel/generator'
|
|
4
|
+
import {
|
|
5
|
+
ChunkDefinition,
|
|
6
|
+
ChunkType,
|
|
7
|
+
ComponentStructure,
|
|
8
|
+
FileType,
|
|
9
|
+
UIDLStateDefinition,
|
|
10
|
+
} from '@teleporthq/teleport-types'
|
|
11
|
+
import { createNextPagesDataSourcePlugin } from '../src/index'
|
|
12
|
+
import {
|
|
13
|
+
bootProductsRoute,
|
|
14
|
+
browserParams,
|
|
15
|
+
buildTimeArguments,
|
|
16
|
+
orderBy,
|
|
17
|
+
rowWindow,
|
|
18
|
+
} from './_helpers/first-page-runtime'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A PLAIN list — no paging, no search — sorted by the dropdown a visitor picks
|
|
22
|
+
* from (`sortBy.split("-")[0]`, a page state starting at "name-asc"). The
|
|
23
|
+
* page's getStaticProps fetched its rows unsorted and the list dropped them, so
|
|
24
|
+
* the server HTML carried its loading slot and the browser fetched the rows on
|
|
25
|
+
* load. The build now fetches them by the sort a visitor starts with, the list
|
|
26
|
+
* shows them while the sort is still there, and any other sort is fetched fresh
|
|
27
|
+
* — the rule the paged lists follow (starting-sort-first-page.test.ts).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const DS_ID = 'ds1'
|
|
31
|
+
const TABLE = 'teleport_products'
|
|
32
|
+
|
|
33
|
+
const STORE_SORT = {
|
|
34
|
+
sort: { type: 'expr', content: `sortBy.split("-")[0] || ''` },
|
|
35
|
+
sortDirection: { type: 'expr', content: `sortBy.split("-")[1] || ''` },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The sort dropdown's state, as the store template declares it.
|
|
39
|
+
const SORT_DROPDOWN: Record<string, UIDLStateDefinition> = {
|
|
40
|
+
sortBy: { type: 'string', defaultValue: 'name-asc', urlSearchParamBinding: { key: 'sortBy' } },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const STARTING_SORTS = `(sortBy => JSON.stringify([{ field: sortBy.split("-")?.[0] || '', order: sortBy.split("-")?.[1] || '' }]))("name-asc")`
|
|
44
|
+
|
|
45
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
46
|
+
|
|
47
|
+
const jsx = (name: string, attrs: types.JSXAttribute[], children: types.JSXElement[] = []) =>
|
|
48
|
+
types.jsxElement(
|
|
49
|
+
types.jsxOpeningElement(types.jsxIdentifier(name), attrs, children.length === 0),
|
|
50
|
+
children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(name)),
|
|
51
|
+
children,
|
|
52
|
+
children.length === 0
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// The provider the component generator emits for a list of six products.
|
|
56
|
+
const makeProvider = (): types.JSXElement =>
|
|
57
|
+
jsx('DataProvider', [
|
|
58
|
+
types.jsxAttribute(
|
|
59
|
+
types.jsxIdentifier('name'),
|
|
60
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
61
|
+
),
|
|
62
|
+
types.jsxAttribute(
|
|
63
|
+
types.jsxIdentifier('resourceDefinition'),
|
|
64
|
+
types.jsxExpressionContainer(
|
|
65
|
+
types.objectExpression([
|
|
66
|
+
types.objectProperty(types.stringLiteral('dataSourceId'), types.stringLiteral(DS_ID)),
|
|
67
|
+
types.objectProperty(types.stringLiteral('tableName'), types.stringLiteral(TABLE)),
|
|
68
|
+
types.objectProperty(
|
|
69
|
+
types.stringLiteral('dataSourceType'),
|
|
70
|
+
types.stringLiteral('teleport')
|
|
71
|
+
),
|
|
72
|
+
])
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
types.jsxAttribute(
|
|
76
|
+
types.jsxIdentifier('params'),
|
|
77
|
+
types.jsxExpressionContainer(
|
|
78
|
+
types.objectExpression([
|
|
79
|
+
types.objectProperty(types.stringLiteral('limit'), types.numericLiteral(6)),
|
|
80
|
+
])
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
types.jsxAttribute(
|
|
84
|
+
types.jsxIdentifier('renderSuccess'),
|
|
85
|
+
types.jsxExpressionContainer(
|
|
86
|
+
types.arrowFunctionExpression(
|
|
87
|
+
[types.identifier('items')],
|
|
88
|
+
jsx('Repeater', [
|
|
89
|
+
types.jsxAttribute(
|
|
90
|
+
types.jsxIdentifier('items'),
|
|
91
|
+
types.jsxExpressionContainer(types.identifier('items'))
|
|
92
|
+
),
|
|
93
|
+
types.jsxAttribute(
|
|
94
|
+
types.jsxIdentifier('renderItem'),
|
|
95
|
+
types.jsxExpressionContainer(
|
|
96
|
+
types.arrowFunctionExpression([types.identifier('product')], jsx('div', []))
|
|
97
|
+
)
|
|
98
|
+
),
|
|
99
|
+
])
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
interface ListOptions {
|
|
106
|
+
sorted?: boolean
|
|
107
|
+
filters?: unknown[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const makeListNode = ({ sorted = true, filters }: ListOptions = {}): any => ({
|
|
111
|
+
type: 'data-source-list',
|
|
112
|
+
content: {
|
|
113
|
+
renderPropIdentifier: 'items',
|
|
114
|
+
resourceDefinition: { dataSourceId: DS_ID, tableName: TABLE, dataSourceType: 'teleport' },
|
|
115
|
+
resource: {
|
|
116
|
+
id: 'TQ_products',
|
|
117
|
+
params: {
|
|
118
|
+
limit: { type: 'static', content: 6 },
|
|
119
|
+
...(filters ? { filters: { type: 'static', content: filters } } : {}),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
nodes: {
|
|
123
|
+
success: {
|
|
124
|
+
type: 'cms-list-repeater',
|
|
125
|
+
content: {
|
|
126
|
+
renderPropIdentifier: 'product',
|
|
127
|
+
paginated: false,
|
|
128
|
+
searchEnabled: false,
|
|
129
|
+
...(sorted ? STORE_SORT : {}),
|
|
130
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
/** A products page drawing the given plain lists, run through the page's data-source plugin. */
|
|
138
|
+
const runPage = async (
|
|
139
|
+
lists: ListOptions[],
|
|
140
|
+
// `null`: the page declares no state, so the build cannot know the sort.
|
|
141
|
+
stateDefinitions: Record<string, UIDLStateDefinition> | null = SORT_DROPDOWN
|
|
142
|
+
) => {
|
|
143
|
+
const providers = lists.map(makeProvider)
|
|
144
|
+
const chunk: ChunkDefinition = {
|
|
145
|
+
name: 'jsx-component',
|
|
146
|
+
type: ChunkType.AST,
|
|
147
|
+
fileType: FileType.JS,
|
|
148
|
+
linkAfter: [],
|
|
149
|
+
content: types.variableDeclaration('const', [
|
|
150
|
+
types.variableDeclarator(
|
|
151
|
+
types.identifier('ProductsList'),
|
|
152
|
+
types.arrowFunctionExpression(
|
|
153
|
+
[types.identifier('props')],
|
|
154
|
+
types.blockStatement([types.returnStatement(jsx('div', [], providers))])
|
|
155
|
+
)
|
|
156
|
+
),
|
|
157
|
+
]),
|
|
158
|
+
meta: { nodesLookup: Object.fromEntries(providers.map((p, i) => [`provider${i}`, p])) },
|
|
159
|
+
}
|
|
160
|
+
const structure: ComponentStructure = {
|
|
161
|
+
uidl: {
|
|
162
|
+
name: 'ProductsList',
|
|
163
|
+
node: {
|
|
164
|
+
type: 'element',
|
|
165
|
+
content: { elementType: 'container', children: lists.map(makeListNode) },
|
|
166
|
+
},
|
|
167
|
+
...(stateDefinitions ? { stateDefinitions } : {}),
|
|
168
|
+
outputOptions: { folderPath: [], fileName: 'products' },
|
|
169
|
+
},
|
|
170
|
+
chunks: [chunk],
|
|
171
|
+
dependencies: {},
|
|
172
|
+
options: {
|
|
173
|
+
dataSources: {
|
|
174
|
+
[DS_ID]: {
|
|
175
|
+
id: DS_ID,
|
|
176
|
+
name: 'TeleportHQ database',
|
|
177
|
+
type: 'teleport',
|
|
178
|
+
config: { selectedTables: { [TABLE]: { columns: [{ name: 'id' }, { name: 'name' }] } } },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
extractedResources: {},
|
|
182
|
+
},
|
|
183
|
+
} as never
|
|
184
|
+
await createNextPagesDataSourcePlugin()(structure)
|
|
185
|
+
const staticPropsChunk = structure.chunks.find((c) => c.name === 'getStaticProps')
|
|
186
|
+
return {
|
|
187
|
+
component: collapse(generator(chunk.content as types.Node).code),
|
|
188
|
+
staticProps: staticPropsChunk
|
|
189
|
+
? collapse(generator(staticPropsChunk.content as types.Node).code)
|
|
190
|
+
: '',
|
|
191
|
+
chunk,
|
|
192
|
+
staticPropsChunk,
|
|
193
|
+
providers,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The provider's `initialData` as the browser evaluates it for the given state.
|
|
198
|
+
const initialDataOf = (provider: types.JSXElement, state: Record<string, unknown>): unknown => {
|
|
199
|
+
const attr = provider.openingElement.attributes.find(
|
|
200
|
+
(a) => a.type === 'JSXAttribute' && a.name.name === 'initialData'
|
|
201
|
+
) as types.JSXAttribute | undefined
|
|
202
|
+
if (!attr) {
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
const expression = (attr.value as types.JSXExpressionContainer).expression as types.Expression
|
|
206
|
+
const names = Object.keys(state)
|
|
207
|
+
return new Function(...names, `return (${generator(expression).code})`)(
|
|
208
|
+
...names.map((name) => state[name])
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
describe('a plain list sorted by a dropdown, on a page', () => {
|
|
213
|
+
it('fetches its rows at build time by the sort a visitor starts with, as a visitor', async () => {
|
|
214
|
+
const { staticProps } = await runPage([{}])
|
|
215
|
+
|
|
216
|
+
// The browser's own sort expression, with the dropdown at its default.
|
|
217
|
+
expect(staticProps).toContain(
|
|
218
|
+
`teleportTeleportProductsDs1.fetchData({ "limit": 6, "sorts": ${STARTING_SORTS} }, { as: "visitor" })`
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('shows those rows while the sort is where a visitor starts', async () => {
|
|
223
|
+
const { component } = await runPage([{}])
|
|
224
|
+
|
|
225
|
+
expect(component).toContain(
|
|
226
|
+
'initialData={sortBy === "name-asc" ? props.items_products : undefined}'
|
|
227
|
+
)
|
|
228
|
+
// Any other sort is another query: the params follow the dropdown.
|
|
229
|
+
expect(component).toMatch(
|
|
230
|
+
/sorts: JSON\.stringify\(\[\{ field: sortBy\.split\("-"\).*\}\), \[sortBy\]\)\}/
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('keeps fetching in the browser when the build cannot know where the sort starts', async () => {
|
|
235
|
+
const { component, staticProps } = await runPage([{}], null)
|
|
236
|
+
|
|
237
|
+
expect(component).not.toContain('initialData')
|
|
238
|
+
expect(staticProps).not.toContain('"sorts"')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('fetches an unsorted list beside it with the same params on its own', async () => {
|
|
242
|
+
const { component, staticProps } = await runPage([{ sorted: false }, {}])
|
|
243
|
+
|
|
244
|
+
expect(staticProps).toContain(
|
|
245
|
+
'const [items_products, items_products_2] = await Promise.all([teleportTeleportProductsDs1.fetchData({ "limit": 6 }, { as: "visitor" })'
|
|
246
|
+
)
|
|
247
|
+
expect(staticProps).toContain(
|
|
248
|
+
`teleportTeleportProductsDs1.fetchData({ "limit": 6, "sorts": ${STARTING_SORTS} }, { as: "visitor" })`
|
|
249
|
+
)
|
|
250
|
+
expect(component).toContain('initialData={props.items_products}')
|
|
251
|
+
expect(component).toContain(
|
|
252
|
+
'initialData={sortBy === "name-asc" ? props.items_products_2 : undefined}'
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('is never seeded while a visitor filters it', async () => {
|
|
257
|
+
const byCategory = {
|
|
258
|
+
type: 'condition',
|
|
259
|
+
source: 'category',
|
|
260
|
+
destination: { type: 'dynamic', content: { referenceType: 'state', id: 'selectedCategory' } },
|
|
261
|
+
operand: '=',
|
|
262
|
+
}
|
|
263
|
+
const { component, staticProps } = await runPage([{ filters: [byCategory] }], {
|
|
264
|
+
...SORT_DROPDOWN,
|
|
265
|
+
selectedCategory: { type: 'string', defaultValue: '' },
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(component).not.toContain('initialData')
|
|
269
|
+
expect(staticProps).not.toContain('fetchData')
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Executed: the rows the build fetches and the ones the browser asks for must
|
|
275
|
+
* be one ordering of one row set, and the build's stand in only for the
|
|
276
|
+
* starting sort.
|
|
277
|
+
*/
|
|
278
|
+
describe('a plain list sorted by a dropdown, run', () => {
|
|
279
|
+
it('sends the build and the browser one sort string while the dropdown is untouched', async () => {
|
|
280
|
+
const { staticPropsChunk, chunk } = await runPage([{}])
|
|
281
|
+
const [atBuild, readAs] = buildTimeArguments(staticPropsChunk)
|
|
282
|
+
|
|
283
|
+
expect(readAs).toEqual({ as: 'visitor' })
|
|
284
|
+
expect(atBuild.sorts).toBe('[{"field":"name","order":"asc"}]')
|
|
285
|
+
expect(browserParams(chunk, { sortBy: 'name-asc' })).toEqual({
|
|
286
|
+
limit: '6',
|
|
287
|
+
sorts: atBuild.sorts,
|
|
288
|
+
})
|
|
289
|
+
expect(browserParams(chunk, { sortBy: 'price-desc' }).sorts).toBe(
|
|
290
|
+
'[{"field":"price","order":"desc"}]'
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('orders the rows at build time exactly as the browser orders them', async () => {
|
|
295
|
+
const { staticPropsChunk, chunk } = await runPage([{}])
|
|
296
|
+
const route = bootProductsRoute()
|
|
297
|
+
|
|
298
|
+
const atBuild = await route.atBuild(buildTimeArguments(staticPropsChunk))
|
|
299
|
+
const inBrowser = await route.inBrowser(browserParams(chunk, { sortBy: 'name-asc' }))
|
|
300
|
+
|
|
301
|
+
// The same columns, rows without a value last, the row's id breaking ties,
|
|
302
|
+
// over the products a visitor is served.
|
|
303
|
+
expect(orderBy(atBuild)).toBe(' ORDER BY name ASC NULLS LAST, "id" ASC LIMIT')
|
|
304
|
+
expect(rowWindow(atBuild)).toBe(' LIMIT 6')
|
|
305
|
+
expect(atBuild).toContain("WHERE (LOWER(TRIM(status)) = 'active')")
|
|
306
|
+
expect(inBrowser).toBe(atBuild)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("shows the build's rows for the starting sort only; any other is fetched fresh", async () => {
|
|
310
|
+
const { providers, staticPropsChunk, chunk } = await runPage([{ sorted: false }, {}])
|
|
311
|
+
const props = { items_products: ['unsorted'], items_products_2: ['by name'] }
|
|
312
|
+
|
|
313
|
+
expect(initialDataOf(providers[1], { sortBy: 'name-asc', props })).toEqual(['by name'])
|
|
314
|
+
// Another pick: no seed, and params the provider has not fetched yet.
|
|
315
|
+
expect(initialDataOf(providers[1], { sortBy: 'price-desc', props })).toBeUndefined()
|
|
316
|
+
expect(browserParams(chunk, { sortBy: 'price-desc' }, 1).sorts).not.toBe(
|
|
317
|
+
buildTimeArguments(staticPropsChunk, 'items_products_2')[0].sorts
|
|
318
|
+
)
|
|
319
|
+
// The unsorted list beside it keeps its own rows, whatever the dropdown says.
|
|
320
|
+
expect(initialDataOf(providers[0], { sortBy: 'price-desc', props })).toEqual(['unsorted'])
|
|
321
|
+
expect(buildTimeArguments(staticPropsChunk, 'items_products')[0]).toEqual({ limit: 6 })
|
|
322
|
+
})
|
|
323
|
+
})
|
|
@@ -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: {
|
|
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
|
+
})
|