@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/_helpers/pagination-widget-fixture.ts +87 -4
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +8 -2
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/fixed-list-sort.test.ts +1 -1
- package/__tests__/infinite-scroll.test.ts +154 -5
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +1 -1
- package/__tests__/product-price-sort.ts +11 -6
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +34 -8
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- 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/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/infinite-scroll.d.ts +25 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +144 -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/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 +137 -101
- 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/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/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/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/infinite-scroll.d.ts +25 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +142 -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/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 +138 -102
- 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/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/package.json +5 -5
- package/src/count-effect.ts +31 -8
- 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 +11 -8
- package/src/infinite-scroll.ts +240 -79
- package/src/list-sort-order.ts +34 -0
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +163 -135
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +5 -4
- package/src/search-input.ts +190 -0
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/index.ts +25 -2
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as types from '@babel/types'
|
|
2
2
|
import { ChunkType, FileType, ChunkDefinition } from '@teleporthq/teleport-types'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PAGINATION_CONTROL_ATTR,
|
|
5
|
+
PAGINATION_FOR_ATTR,
|
|
6
|
+
RANGE_TEMPLATE_ATTR,
|
|
7
|
+
} from '../../src/pagination-controls'
|
|
8
|
+
import { SEARCH_FOR_ATTR } from '../../src/search-input'
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* A component shell carrying a `DataProvider` and the pagination controls a
|
|
@@ -20,9 +25,36 @@ interface ControlOptions {
|
|
|
20
25
|
legacyOnly?: boolean
|
|
21
26
|
/** Number of DataProvider + widget pairs to emit (order-based zip coverage). */
|
|
22
27
|
mappers?: number
|
|
28
|
+
/** The element the providers render in: a table's body, a list. */
|
|
29
|
+
rootTag?: string
|
|
30
|
+
/**
|
|
31
|
+
* The `data-tq-range-template` of a `range` control in the widget. Defaults
|
|
32
|
+
* to the "Showing" wording; `null` authors the control with no template.
|
|
33
|
+
*/
|
|
34
|
+
rangeTemplate?: string | null
|
|
35
|
+
/**
|
|
36
|
+
* A `range` text OUTSIDE every widget, placed BEFORE the first provider (a
|
|
37
|
+
* toolbar above the list), naming its mapper with `data-tq-pagination-for`.
|
|
38
|
+
*/
|
|
39
|
+
standaloneRange?: { forNodeId: string; template?: string }
|
|
40
|
+
/**
|
|
41
|
+
* Search inputs OUTSIDE every widget, placed BEFORE the first provider, in
|
|
42
|
+
* this order: a page's own box naming its mapper with `data-tq-search-for`,
|
|
43
|
+
* or the platform's (`search-input` class) paired by order.
|
|
44
|
+
*/
|
|
45
|
+
standaloneSearch?: Array<{ forNodeId?: string; name: string }>
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
// ASCII hyphen on purpose: the generator prints string literals through jsesc,
|
|
49
|
+
// which would turn an en dash into `–` and make every assertion unreadable.
|
|
50
|
+
export const RANGE_DRAWN_TEXT = 'Showing 1-8 of 12'
|
|
51
|
+
export const RANGE_TEMPLATE = 'Showing {from}–{to} of {total}'
|
|
52
|
+
|
|
53
|
+
const control = (
|
|
54
|
+
marker: string,
|
|
55
|
+
children: types.JSXElement['children'] = [],
|
|
56
|
+
extraAttrs: Array<[string, string]> = []
|
|
57
|
+
): types.JSXElement =>
|
|
26
58
|
types.jsxElement(
|
|
27
59
|
types.jsxOpeningElement(
|
|
28
60
|
types.jsxIdentifier('div'),
|
|
@@ -35,6 +67,9 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
|
|
|
35
67
|
types.jsxIdentifier(PAGINATION_CONTROL_ATTR),
|
|
36
68
|
types.stringLiteral(marker)
|
|
37
69
|
),
|
|
70
|
+
...extraAttrs.map(([name, value]) =>
|
|
71
|
+
types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
|
|
72
|
+
),
|
|
38
73
|
],
|
|
39
74
|
false
|
|
40
75
|
),
|
|
@@ -43,6 +78,40 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
|
|
|
43
78
|
false
|
|
44
79
|
)
|
|
45
80
|
|
|
81
|
+
const rangeControl = (
|
|
82
|
+
template: string | null | undefined,
|
|
83
|
+
forNodeId?: string
|
|
84
|
+
): types.JSXElement => {
|
|
85
|
+
const attrs: Array<[string, string]> = []
|
|
86
|
+
if (template !== null) {
|
|
87
|
+
attrs.push([RANGE_TEMPLATE_ATTR, template ?? RANGE_TEMPLATE])
|
|
88
|
+
}
|
|
89
|
+
if (forNodeId) {
|
|
90
|
+
attrs.push([PAGINATION_FOR_ATTR, forNodeId])
|
|
91
|
+
}
|
|
92
|
+
return control('range', [types.jsxText(RANGE_DRAWN_TEXT)], attrs)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const searchInput = (name: string, forNodeId?: string): types.JSXElement =>
|
|
96
|
+
types.jsxElement(
|
|
97
|
+
types.jsxOpeningElement(
|
|
98
|
+
types.jsxIdentifier('input'),
|
|
99
|
+
[
|
|
100
|
+
types.jsxAttribute(types.jsxIdentifier('name'), types.stringLiteral(name)),
|
|
101
|
+
forNodeId
|
|
102
|
+
? types.jsxAttribute(types.jsxIdentifier(SEARCH_FOR_ATTR), types.stringLiteral(forNodeId))
|
|
103
|
+
: types.jsxAttribute(
|
|
104
|
+
types.jsxIdentifier('className'),
|
|
105
|
+
types.stringLiteral('products-list-search-input')
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
true
|
|
109
|
+
),
|
|
110
|
+
null,
|
|
111
|
+
[],
|
|
112
|
+
true
|
|
113
|
+
)
|
|
114
|
+
|
|
46
115
|
const legacyControl = (direction: 'previous' | 'next'): types.JSXElement =>
|
|
47
116
|
types.jsxElement(
|
|
48
117
|
types.jsxOpeningElement(
|
|
@@ -72,6 +141,10 @@ const buildWidget = (options: ControlOptions): types.JSXElement => {
|
|
|
72
141
|
children.push(legacyControl(marker))
|
|
73
142
|
continue
|
|
74
143
|
}
|
|
144
|
+
if (marker === 'range') {
|
|
145
|
+
children.push(rangeControl(options.rangeTemplate))
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
75
148
|
children.push(control(marker))
|
|
76
149
|
}
|
|
77
150
|
|
|
@@ -148,6 +221,12 @@ const buildDataProvider = (name: string, rowIdentifier: string): types.JSXElemen
|
|
|
148
221
|
|
|
149
222
|
export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefinition => {
|
|
150
223
|
const sections: types.JSXElement[] = []
|
|
224
|
+
if (options.standaloneRange) {
|
|
225
|
+
sections.push(rangeControl(options.standaloneRange.template, options.standaloneRange.forNodeId))
|
|
226
|
+
}
|
|
227
|
+
for (const search of options.standaloneSearch ?? []) {
|
|
228
|
+
sections.push(searchInput(search.name, search.forNodeId))
|
|
229
|
+
}
|
|
151
230
|
const mappers = options.mappers ?? 1
|
|
152
231
|
for (let i = 0; i < mappers; i += 1) {
|
|
153
232
|
const identifier = `items${i === 0 ? '' : i}`
|
|
@@ -155,9 +234,10 @@ export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefiniti
|
|
|
155
234
|
sections.push(buildWidget(options))
|
|
156
235
|
}
|
|
157
236
|
|
|
237
|
+
const rootTag = options.rootTag ?? 'div'
|
|
158
238
|
const root = types.jsxElement(
|
|
159
|
-
types.jsxOpeningElement(types.jsxIdentifier(
|
|
160
|
-
types.jsxClosingElement(types.jsxIdentifier(
|
|
239
|
+
types.jsxOpeningElement(types.jsxIdentifier(rootTag), [], false),
|
|
240
|
+
types.jsxClosingElement(types.jsxIdentifier(rootTag)),
|
|
161
241
|
sections,
|
|
162
242
|
false
|
|
163
243
|
)
|
|
@@ -189,6 +269,8 @@ export interface RepeaterOptions {
|
|
|
189
269
|
perPage?: number
|
|
190
270
|
/** The list's filters, as the inspector authors them (conditions and groups). */
|
|
191
271
|
filters?: unknown[]
|
|
272
|
+
/** The mapper's editor node id, as the UIDL carries it on the repeater. */
|
|
273
|
+
nodeId?: string
|
|
192
274
|
}
|
|
193
275
|
|
|
194
276
|
export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'items'): any => ({
|
|
@@ -218,6 +300,7 @@ export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'ite
|
|
|
218
300
|
...(options.paginationMode ? { paginationMode: options.paginationMode } : {}),
|
|
219
301
|
...(options.infiniteScroll ? { infiniteScroll: true } : {}),
|
|
220
302
|
...(options.infiniteScrollLoadMore ? { infiniteScrollLoadMore: true } : {}),
|
|
303
|
+
...(options.nodeId ? { nodeId: options.nodeId } : {}),
|
|
221
304
|
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
222
305
|
},
|
|
223
306
|
},
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { generateBookingItemTransformationCode } from '../src/transformations/booking-item'
|
|
2
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
3
|
+
import {
|
|
4
|
+
detectTransformationType,
|
|
5
|
+
getTransformExpression,
|
|
6
|
+
getTransformWrapperCode,
|
|
7
|
+
} from '../src/transformations'
|
|
8
|
+
|
|
9
|
+
// A booking page binds an item's columns as stored; the transform only puts
|
|
10
|
+
// its texts in the page's language. These evaluate the EMITTED code, the way
|
|
11
|
+
// the runtime does.
|
|
12
|
+
|
|
13
|
+
type Build = (record: unknown, options?: Record<string, unknown>) => Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
const builderFor = (code: string): Build =>
|
|
16
|
+
new Function(
|
|
17
|
+
generateSharedTransformationCode() + '\n' + code + '\nreturn buildBookingItem;'
|
|
18
|
+
)() as Build
|
|
19
|
+
|
|
20
|
+
const buildBookingItem = builderFor(generateBookingItemTransformationCode())
|
|
21
|
+
|
|
22
|
+
const buildWithCategories = builderFor(
|
|
23
|
+
generateBookingItemTransformationCode({
|
|
24
|
+
categories: [
|
|
25
|
+
{
|
|
26
|
+
id: 'rooms',
|
|
27
|
+
name: 'Rooms',
|
|
28
|
+
slug: 'rooms',
|
|
29
|
+
parentId: null,
|
|
30
|
+
order: 0,
|
|
31
|
+
translations: { es: { name: 'Habitaciones' } },
|
|
32
|
+
children: [
|
|
33
|
+
{
|
|
34
|
+
id: 'suites',
|
|
35
|
+
name: 'Suites',
|
|
36
|
+
slug: 'suites',
|
|
37
|
+
parentId: 'rooms',
|
|
38
|
+
order: 0,
|
|
39
|
+
children: [],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const item = {
|
|
48
|
+
id: 'i1',
|
|
49
|
+
slug: 'double-room',
|
|
50
|
+
name: 'Double room',
|
|
51
|
+
es_name: 'Habitación doble',
|
|
52
|
+
description: '<p>Sea view.</p>',
|
|
53
|
+
es_description: '<p>Vista al mar.</p>',
|
|
54
|
+
location: 'Main street',
|
|
55
|
+
es_location: '',
|
|
56
|
+
price: '120.00',
|
|
57
|
+
booking_unit: 'night',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('the booking item transform', () => {
|
|
61
|
+
it('is applied to the booking items table only', () => {
|
|
62
|
+
expect(detectTransformationType('teleport_booking_items')).toBe('booking-item')
|
|
63
|
+
expect(detectTransformationType('public.TELEPORT_BOOKING_ITEMS')).toBe('booking-item')
|
|
64
|
+
expect(detectTransformationType('booking_items')).toBeNull()
|
|
65
|
+
expect(getTransformExpression('teleport_booking_items')).toContain('transformRecords')
|
|
66
|
+
expect(getTransformWrapperCode('teleport_booking_items')).toContain('transformBookingItems')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('shows the texts in the page’s language, every column kept as stored', () => {
|
|
70
|
+
const row = buildBookingItem(item, { currentLanguage: 'es', mainLanguage: 'en' })
|
|
71
|
+
expect(row).toMatchObject({
|
|
72
|
+
name: 'Habitación doble',
|
|
73
|
+
description: '<p>Vista al mar.</p>',
|
|
74
|
+
// Not translated yet: the main language's.
|
|
75
|
+
location: 'Main street',
|
|
76
|
+
slug: 'double-room',
|
|
77
|
+
price: '120.00',
|
|
78
|
+
booking_unit: 'night',
|
|
79
|
+
es_name: 'Habitación doble',
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('names its category the way the taxonomy names its first one in the page’s language', () => {
|
|
84
|
+
const filed = { ...item, category: 'Rooms', category_ids: '["rooms","suites"]' }
|
|
85
|
+
expect(buildWithCategories(filed, { currentLanguage: 'es', mainLanguage: 'en' }).category).toBe(
|
|
86
|
+
'Habitaciones'
|
|
87
|
+
)
|
|
88
|
+
expect(buildWithCategories(filed, { currentLanguage: 'en', mainLanguage: 'en' }).category).toBe(
|
|
89
|
+
'Rooms'
|
|
90
|
+
)
|
|
91
|
+
// No translation: the main name; a nested category resolves too.
|
|
92
|
+
expect(
|
|
93
|
+
buildWithCategories(
|
|
94
|
+
{ ...filed, category: 'Suites', category_ids: '["suites"]' },
|
|
95
|
+
{ currentLanguage: 'es', mainLanguage: 'en' }
|
|
96
|
+
).category
|
|
97
|
+
).toBe('Suites')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('keeps the stored category while none of its ids is a category', () => {
|
|
101
|
+
const legacy = { ...item, category: 'Rooms', category_ids: '["gone"]' }
|
|
102
|
+
expect(
|
|
103
|
+
buildWithCategories(legacy, { currentLanguage: 'es', mainLanguage: 'en' }).category
|
|
104
|
+
).toBe('Rooms')
|
|
105
|
+
expect(buildBookingItem(legacy, { currentLanguage: 'es', mainLanguage: 'en' }).category).toBe(
|
|
106
|
+
'Rooms'
|
|
107
|
+
)
|
|
108
|
+
// A row without category columns gains none.
|
|
109
|
+
expect(
|
|
110
|
+
'category' in buildWithCategories(item, { currentLanguage: 'es', mainLanguage: 'en' })
|
|
111
|
+
).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('keeps the main language on its own pages and without a localization', () => {
|
|
115
|
+
expect(buildBookingItem(item, { currentLanguage: 'en', mainLanguage: 'en' }).name).toBe(
|
|
116
|
+
'Double room'
|
|
117
|
+
)
|
|
118
|
+
expect(buildBookingItem(item, {}).name).toBe('Double room')
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -67,7 +67,10 @@ function bootTableRoute(tableName: string, options: BootOptions = {}) {
|
|
|
67
67
|
options.cache
|
|
68
68
|
)
|
|
69
69
|
const code = esm
|
|
70
|
-
.replace(
|
|
70
|
+
.replace(
|
|
71
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
72
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
73
|
+
)
|
|
71
74
|
.replace(
|
|
72
75
|
"import { tqWithCache } from '../tq-cache/server'",
|
|
73
76
|
"const { tqWithCache } = require('tq-cache')"
|
|
@@ -417,7 +420,10 @@ describe('raw-query state routes bind {{Current User.*}} to the session', () =>
|
|
|
417
420
|
paramFields
|
|
418
421
|
)
|
|
419
422
|
const code = esm
|
|
420
|
-
.replace(
|
|
423
|
+
.replace(
|
|
424
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
425
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
426
|
+
)
|
|
421
427
|
.replace(/^export default async function handler/m, 'async function handler')
|
|
422
428
|
.concat('\nmodule.exports = { handler: handler }')
|
|
423
429
|
const queries: Array<{ sql: string; params: unknown[] }> = []
|
|
@@ -335,7 +335,7 @@ describe('generateDataSourceFetcher', () => {
|
|
|
335
335
|
const dataSource = createPostgreSQLDataSource('ds-1')
|
|
336
336
|
const code = generateDataSourceFetcher(dataSource, 'users')
|
|
337
337
|
|
|
338
|
-
expect(code).toContain('import { Client } from')
|
|
338
|
+
expect(code).toContain('import { Client, types as pgTypes } from')
|
|
339
339
|
expect(code).toContain('export default async function handler')
|
|
340
340
|
expect(code).toContain('SELECT * FROM "users"')
|
|
341
341
|
expect(code).toContain('localhost')
|
|
@@ -157,7 +157,7 @@ describe('a list sort the editor fixed', () => {
|
|
|
157
157
|
const code = await runPlugin('state', CACHE)
|
|
158
158
|
expect(code).not.toContain('initialData=')
|
|
159
159
|
expect(code).not.toContain('ds_0_cached')
|
|
160
|
-
expect(code).toContain('field: sortBy.split("-")[0]')
|
|
160
|
+
expect(code).toContain('field: sortBy.split("-")?.[0]')
|
|
161
161
|
})
|
|
162
162
|
})
|
|
163
163
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
1
2
|
import * as types from '@babel/types'
|
|
2
3
|
import generator from '@babel/generator'
|
|
3
4
|
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
5
|
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
6
|
+
import {
|
|
7
|
+
buildAccumulatingRequestMark,
|
|
8
|
+
buildAccumulatingResponseHandler,
|
|
9
|
+
getInfiniteScrollVars,
|
|
10
|
+
} from '../src/infinite-scroll'
|
|
5
11
|
import {
|
|
6
12
|
makeMultiMapperUidl,
|
|
7
13
|
makeUidlNode,
|
|
@@ -11,9 +17,10 @@ import {
|
|
|
11
17
|
|
|
12
18
|
const runPlugin = async (
|
|
13
19
|
repeater: RepeaterOptions,
|
|
14
|
-
controls: string[] = ['previous', 'next']
|
|
20
|
+
controls: string[] = ['previous', 'next'],
|
|
21
|
+
rootTag?: string
|
|
15
22
|
): Promise<string> => {
|
|
16
|
-
const chunk = makeWidgetComponentChunk({ controls })
|
|
23
|
+
const chunk = makeWidgetComponentChunk({ controls, rootTag })
|
|
17
24
|
const structure: ComponentStructure = {
|
|
18
25
|
uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
|
|
19
26
|
chunks: [chunk],
|
|
@@ -31,7 +38,7 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
31
38
|
const code = await runPlugin({ infiniteScroll: true })
|
|
32
39
|
const flat = collapse(code)
|
|
33
40
|
|
|
34
|
-
expect(flat).toContain('const ds_0_accumRef = useRef({ sig: "", pages: {} })')
|
|
41
|
+
expect(flat).toContain('const ds_0_accumRef = useRef({ sig: "", pages: {}, latest: null })')
|
|
35
42
|
expect(flat).toContain('const [ds_0_hasMoreFlag, setDs_0_hasMoreFlag] = useState(true)')
|
|
36
43
|
// Two independent end-of-data signals: a short page, and the counted total.
|
|
37
44
|
expect(flat).toContain(
|
|
@@ -42,12 +49,18 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
42
49
|
// change must start a fresh list rather than interleave two result sets.
|
|
43
50
|
expect(flat).toContain('const sig = JSON.stringify({ ...params, page: 0 })')
|
|
44
51
|
expect(flat).toContain('if (ds_0_accumRef.current.sig !== sig)')
|
|
45
|
-
expect(flat).toContain('ds_0_accumRef.current = {
|
|
52
|
+
expect(flat).toContain('ds_0_accumRef.current.pages = {}')
|
|
46
53
|
expect(flat).toContain('ds_0_accumRef.current.pages[page] = rows')
|
|
54
|
+
// fetchData records the request the list stands on before making it.
|
|
55
|
+
expect(flat).toContain(
|
|
56
|
+
'ds_0_accumRef.current.latest = { sig: JSON.stringify({ ...params, page: 0 }), page: Number(params.page) || 1 }'
|
|
57
|
+
)
|
|
47
58
|
// A short page is the only end signal a source that cannot count gives.
|
|
48
59
|
expect(flat).toContain('setDs_0_hasMoreFlag(rows.length >= 20)')
|
|
49
60
|
// Numeric sort, so page 10 does not land between 1 and 2.
|
|
50
|
-
expect(flat).
|
|
61
|
+
expect(flat).toMatch(
|
|
62
|
+
/\.map\(Number\)\.filter\(\(?p\)? => p <= last\)\.sort\(\(a, b\) => a - b\)\.reduce\(/
|
|
63
|
+
)
|
|
51
64
|
})
|
|
52
65
|
|
|
53
66
|
it('keeps the page out of the provider key and suppresses initialData', async () => {
|
|
@@ -82,6 +95,30 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
82
95
|
)
|
|
83
96
|
})
|
|
84
97
|
|
|
98
|
+
it('builds the sentinel as an element its parent may hold', async () => {
|
|
99
|
+
// A <div> among a table's rows is moved out of the table by the parser:
|
|
100
|
+
// the server's markup and the client's tree then differ and hydration fails.
|
|
101
|
+
const inBody = collapse(
|
|
102
|
+
await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'tbody')
|
|
103
|
+
)
|
|
104
|
+
expect(inBody).toContain(
|
|
105
|
+
'<tr ref={ds_0_sentinelRef} aria-hidden="true" style={{ height: "1px" }}><td style={{ padding: 0, border: 0 }} /></tr>'
|
|
106
|
+
)
|
|
107
|
+
expect(inBody).not.toContain('<div ref={ds_0_sentinelRef}')
|
|
108
|
+
|
|
109
|
+
const inList = collapse(await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'ul'))
|
|
110
|
+
expect(inList).toContain(
|
|
111
|
+
'<li ref={ds_0_sentinelRef} aria-hidden="true" style={{ height: "1px", listStyle: "none" }} />'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const inParagraph = collapse(
|
|
115
|
+
await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'p')
|
|
116
|
+
)
|
|
117
|
+
expect(inParagraph).toContain(
|
|
118
|
+
'<span ref={ds_0_sentinelRef} aria-hidden="true" style={{ display: "block", height: "1px" }} />'
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
85
122
|
it('wires the authored Load More button instead, with no observer', async () => {
|
|
86
123
|
const code = await runPlugin({ infiniteScroll: true, infiniteScrollLoadMore: true }, [
|
|
87
124
|
'load-more',
|
|
@@ -141,3 +178,115 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
141
178
|
expect(code).not.toContain('setDs_0_page(page => Math.max(1, page - 1))')
|
|
142
179
|
})
|
|
143
180
|
})
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The accumulator, executed: `fetchData` as the page emits it, answered in
|
|
184
|
+
* the orders a browser can produce. A sort (filter, search) change made on
|
|
185
|
+
* page 2 fires page 2 under the new query first, then page 1 once the page is
|
|
186
|
+
* reset — and `DataProvider` renders whichever answer lands last.
|
|
187
|
+
*/
|
|
188
|
+
describe('the appending list, run', () => {
|
|
189
|
+
const PER_PAGE = 2
|
|
190
|
+
|
|
191
|
+
interface Deferred {
|
|
192
|
+
params: { page: number; sorts: string }
|
|
193
|
+
resolve: (rows: string[]) => void
|
|
194
|
+
result: Promise<string[]>
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function bootList() {
|
|
198
|
+
const vars = getInfiniteScrollVars(0)
|
|
199
|
+
const mark = generator(types.program([buildAccumulatingRequestMark(vars)])).code
|
|
200
|
+
const handler = generator(buildAccumulatingResponseHandler(vars, PER_PAGE)).code
|
|
201
|
+
const accumRef = { current: { sig: '', pages: {}, latest: null } }
|
|
202
|
+
const hasMore: boolean[] = []
|
|
203
|
+
const fetchData = new Function(
|
|
204
|
+
'ds_0_accumRef',
|
|
205
|
+
'setDs_0_hasMoreFlag',
|
|
206
|
+
'answer',
|
|
207
|
+
`return function fetchData(params) { ${mark}; return answer(params).then(${handler}) }`
|
|
208
|
+
)(
|
|
209
|
+
accumRef,
|
|
210
|
+
(value: boolean) => hasMore.push(value),
|
|
211
|
+
(params: Deferred['params']) => {
|
|
212
|
+
let resolve: (rows: string[]) => void = () => undefined
|
|
213
|
+
const answered = new Promise<{ data: string[] }>((done) => {
|
|
214
|
+
resolve = (rows) => done({ data: rows })
|
|
215
|
+
})
|
|
216
|
+
requests.push({ params, resolve, result: undefined as never })
|
|
217
|
+
return answered
|
|
218
|
+
}
|
|
219
|
+
) as (params: Deferred['params']) => Promise<string[]>
|
|
220
|
+
const requests: Deferred[] = []
|
|
221
|
+
const request = (page: number, sorts: string): Deferred => {
|
|
222
|
+
const result = fetchData({ page, sorts })
|
|
223
|
+
const made = requests[requests.length - 1]
|
|
224
|
+
made.result = result
|
|
225
|
+
return made
|
|
226
|
+
}
|
|
227
|
+
return { request, hasMore }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const rowsOf = (sorts: string, page: number) => [`${sorts}${page}a`, `${sorts}${page}b`]
|
|
231
|
+
|
|
232
|
+
it('restarts at page 1 after a sort change on page 2, and loads page 2 on the next click', async () => {
|
|
233
|
+
const list = bootList()
|
|
234
|
+
const first = list.request(1, 'A')
|
|
235
|
+
first.resolve(rowsOf('A', 1))
|
|
236
|
+
const second = list.request(2, 'A')
|
|
237
|
+
second.resolve(rowsOf('A', 2))
|
|
238
|
+
expect(await second.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
239
|
+
|
|
240
|
+
const stray = list.request(2, 'B')
|
|
241
|
+
const restart = list.request(1, 'B')
|
|
242
|
+
stray.resolve(rowsOf('B', 2))
|
|
243
|
+
// The stray page changes nothing: the list on show stays until page 1 answers.
|
|
244
|
+
expect(await stray.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
245
|
+
restart.resolve(rowsOf('B', 1))
|
|
246
|
+
expect(await restart.result).toEqual(rowsOf('B', 1))
|
|
247
|
+
|
|
248
|
+
const next = list.request(2, 'B')
|
|
249
|
+
next.resolve(rowsOf('B', 2))
|
|
250
|
+
expect(await next.result).toEqual([...rowsOf('B', 1), ...rowsOf('B', 2)])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('keeps the restarted list when the stray page answers last', async () => {
|
|
254
|
+
const list = bootList()
|
|
255
|
+
list.request(1, 'A').resolve(rowsOf('A', 1))
|
|
256
|
+
const second = list.request(2, 'A')
|
|
257
|
+
second.resolve(rowsOf('A', 2))
|
|
258
|
+
await second.result
|
|
259
|
+
|
|
260
|
+
const stray = list.request(2, 'B')
|
|
261
|
+
const restart = list.request(1, 'B')
|
|
262
|
+
restart.resolve(rowsOf('B', 1))
|
|
263
|
+
expect(await restart.result).toEqual(rowsOf('B', 1))
|
|
264
|
+
stray.resolve(rowsOf('B', 2))
|
|
265
|
+
expect(await stray.result).toEqual(rowsOf('B', 1))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('never lets a late answer to an earlier query replace the list', async () => {
|
|
269
|
+
const list = bootList()
|
|
270
|
+
const before = list.request(1, 'A')
|
|
271
|
+
const after = list.request(1, 'B')
|
|
272
|
+
after.resolve(rowsOf('B', 1))
|
|
273
|
+
expect(await after.result).toEqual(rowsOf('B', 1))
|
|
274
|
+
before.resolve(rowsOf('A', 1))
|
|
275
|
+
expect(await before.result).toEqual(rowsOf('B', 1))
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('ends the list on a short page of the request it stands on only', async () => {
|
|
279
|
+
const list = bootList()
|
|
280
|
+
const stray = list.request(2, 'A')
|
|
281
|
+
const page1 = list.request(1, 'A')
|
|
282
|
+
stray.resolve(['A2a'])
|
|
283
|
+
await stray.result
|
|
284
|
+
expect(list.hasMore).toEqual([])
|
|
285
|
+
page1.resolve(rowsOf('A', 1))
|
|
286
|
+
await page1.result
|
|
287
|
+
const page2 = list.request(2, 'A')
|
|
288
|
+
page2.resolve(['A2a'])
|
|
289
|
+
expect(await page2.result).toEqual([...rowsOf('A', 1), 'A2a'])
|
|
290
|
+
expect(list.hasMore).toEqual([true, false])
|
|
291
|
+
})
|
|
292
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import { generateDataSourceFetcher } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the SQL routes of the platform's listings order what a visitor's sort
|
|
6
|
+
* leaves tied or empty: a post without a reading time comes after the others
|
|
7
|
+
* whichever way the list is sorted ("longest read first" opened on them), and
|
|
8
|
+
* equal rows keep one order across pages.
|
|
9
|
+
*
|
|
10
|
+
* Pinned on the route the project generator ships (`generateDataSourceFetcher`),
|
|
11
|
+
* for both databases a blog can live in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ROUTES = [
|
|
15
|
+
['teleport', { host: 'db', database: 'blog' }],
|
|
16
|
+
['postgresql', { connectionString: 'postgres://user:pw@host:5432/db' }],
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
const routeFor = (type: string, config: object, tableName: string): string =>
|
|
20
|
+
generateDataSourceFetcher(
|
|
21
|
+
{ type, config } as unknown as Parameters<typeof generateDataSourceFetcher>[0],
|
|
22
|
+
tableName
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const orderByLines = (route: string): string[] =>
|
|
26
|
+
route.split('\n').filter((line) => line.includes('ORDER BY') && line.includes('Clauses.join'))
|
|
27
|
+
|
|
28
|
+
describe.each(ROUTES)('the %s route of the blog listing', (type, config) => {
|
|
29
|
+
const route = routeFor(type, config, 'teleport_blog_posts')
|
|
30
|
+
|
|
31
|
+
it('parses as valid JavaScript', () => {
|
|
32
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('puts the rows without a value last, in both directions', () => {
|
|
36
|
+
expect(route).toMatch(/\$\{orderOf\(sort\)\} NULLS LAST`/)
|
|
37
|
+
expect(route).toMatch(/\$\{sortByDirection\} NULLS LAST\$\{tiebreak\}`/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('falls back to a plain ORDER BY when the database rejects it', () => {
|
|
41
|
+
expect(route).toMatch(/plainClauses = valid\.map\(\(sort\) => `[^`]*\$\{orderOf\(sort\)\}`\)/)
|
|
42
|
+
expect(route).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
43
|
+
expect(route).toContain('if (!usedRewrittenSort) throw sortError')
|
|
44
|
+
expect(route).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('breaks ties by id, in the ORDER BY and in its fallback', () => {
|
|
48
|
+
expect(route).toContain(`fields.includes("id") ? '' : ", \\"id\\" " + direction`)
|
|
49
|
+
const lines = orderByLines(route)
|
|
50
|
+
expect(lines).toHaveLength(2)
|
|
51
|
+
lines.forEach((line) => expect(line).toMatch(/\$\{tiebreak\}/))
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('a table no listing sorts for visitors', () => {
|
|
56
|
+
it('gets the empty-values-last order but no tiebreaker', () => {
|
|
57
|
+
const route = routeFor('teleport', { host: 'db', database: 'store' }, 'teleport_orders')
|
|
58
|
+
expect(route).toMatch(/\$\{orderOf\(sort\)\} NULLS LAST`/)
|
|
59
|
+
expect(route).toContain("const rowOrderTiebreak = () => ''")
|
|
60
|
+
})
|
|
61
|
+
})
|