@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.
- package/__tests__/_helpers/first-page-runtime.ts +184 -0
- package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +210 -4
- package/__tests__/data-source-request-locale.test.ts +4 -3
- package/__tests__/fetchers.test.ts +7 -3
- package/__tests__/fixed-list-sort.test.ts +1 -1
- package/__tests__/infinite-scroll.test.ts +363 -9
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/page-data-visitor-read.test.ts +196 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +2 -2
- package/__tests__/plain-list-starting-sort.test.ts +323 -0
- package/__tests__/product-price-sort.ts +11 -6
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/refetch-loading-state.test.ts +158 -2
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +34 -8
- package/__tests__/starting-sort-first-page.test.ts +285 -0
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- 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/count-effect.d.ts +7 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +15 -5
- package/dist/cjs/count-effect.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/pg-client-import.d.ts +12 -0
- package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/cjs/fetchers/pg-client-import.js +15 -0
- package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +2 -1
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -2
- 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 +89 -4
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +243 -33
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/list-sort-order.d.ts +14 -0
- package/dist/cjs/list-sort-order.d.ts.map +1 -0
- package/dist/cjs/list-sort-order.js +36 -0
- package/dist/cjs/list-sort-order.js.map +1 -0
- 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-controls.d.ts +22 -3
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +29 -3
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +336 -164
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/range-control.d.ts +45 -0
- package/dist/cjs/range-control.d.ts.map +1 -0
- package/dist/cjs/range-control.js +165 -0
- package/dist/cjs/range-control.js.map +1 -0
- package/dist/cjs/row-order.d.ts +3 -2
- package/dist/cjs/row-order.d.ts.map +1 -1
- package/dist/cjs/row-order.js +5 -4
- package/dist/cjs/row-order.js.map +1 -1
- package/dist/cjs/search-input.d.ts +42 -0
- package/dist/cjs/search-input.d.ts.map +1 -0
- package/dist/cjs/search-input.js +158 -0
- package/dist/cjs/search-input.js.map +1 -0
- 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/transformations/booking-item.d.ts +24 -0
- package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
- package/dist/cjs/transformations/booking-item.js +27 -0
- package/dist/cjs/transformations/booking-item.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +4 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -5
- package/dist/cjs/transformations/index.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/count-effect.d.ts +7 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +15 -5
- package/dist/esm/count-effect.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/pg-client-import.d.ts +12 -0
- package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/esm/fetchers/pg-client-import.js +12 -0
- package/dist/esm/fetchers/pg-client-import.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +2 -1
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -2
- 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 +89 -4
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +237 -32
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/list-sort-order.d.ts +14 -0
- package/dist/esm/list-sort-order.d.ts.map +1 -0
- package/dist/esm/list-sort-order.js +32 -0
- package/dist/esm/list-sort-order.js.map +1 -0
- 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-controls.d.ts +22 -3
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +28 -2
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +340 -168
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/range-control.d.ts +45 -0
- package/dist/esm/range-control.d.ts.map +1 -0
- package/dist/esm/range-control.js +137 -0
- package/dist/esm/range-control.js.map +1 -0
- package/dist/esm/row-order.d.ts +3 -2
- package/dist/esm/row-order.d.ts.map +1 -1
- package/dist/esm/row-order.js +5 -4
- package/dist/esm/row-order.js.map +1 -1
- package/dist/esm/search-input.d.ts +42 -0
- package/dist/esm/search-input.d.ts.map +1 -0
- package/dist/esm/search-input.js +129 -0
- package/dist/esm/search-input.js.map +1 -0
- 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/transformations/booking-item.d.ts +24 -0
- package/dist/esm/transformations/booking-item.d.ts.map +1 -0
- package/dist/esm/transformations/booking-item.js +23 -0
- package/dist/esm/transformations/booking-item.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +4 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -5
- package/dist/esm/transformations/index.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 +5 -5
- package/src/cache/ast.ts +4 -1
- package/src/count-effect.ts +31 -8
- package/src/data-source-fetchers.ts +33 -14
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +15 -9
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/teleport.ts +19 -10
- package/src/fetchers/utils/browser-row-policy.ts +79 -5
- package/src/index.ts +8 -2
- package/src/infinite-scroll.ts +420 -81
- package/src/list-sort-order.ts +34 -0
- package/src/loading-state.ts +85 -13
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +532 -218
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +5 -4
- package/src/search-input.ts +190 -0
- package/src/sort-utils.ts +164 -21
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/index.ts +25 -2
- 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 {from}–{to} of {total}',
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
expect(flat).not.toContain('`Showing {')
|
|
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: {
|
|
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
|
-
|
|
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,
|
|
112
|
-
// functional setState bail-out (loop-free), then
|
|
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); };'
|