@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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_RANGE_TEMPLATE,
|
|
4
|
+
PAGINATION_CONTROL_ATTR,
|
|
5
|
+
RANGE_TEMPLATE_ATTR,
|
|
6
|
+
readJSXAttributeString,
|
|
7
|
+
} from './pagination-controls'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The `range` control: a text element ("Showing 1–8 of 12") filled with the
|
|
11
|
+
* live row range of the mapper it belongs to.
|
|
12
|
+
*
|
|
13
|
+
* The author's words stay theirs — they are read off `data-tq-range-template`
|
|
14
|
+
* with `{from}`, `{to}` and `{total}` standing for the numbers — and the drawn
|
|
15
|
+
* text is kept, verbatim, until the count endpoint has answered: `ds_N_total`
|
|
16
|
+
* is `null` until then, and a range of `NaN` or `undefined` is worse than the
|
|
17
|
+
* placeholder the designer chose.
|
|
18
|
+
*
|
|
19
|
+
* buttons / numbered from = total === 0 ? 0 : (page − 1) × perPage + 1
|
|
20
|
+
* infinite scroll from = total === 0 ? 0 : 1
|
|
21
|
+
* both to = min(page × perPage, total)
|
|
22
|
+
*
|
|
23
|
+
* In an appending list `page` is the number of pages loaded so far, so the
|
|
24
|
+
* same `to` counts the rows on screen. Numbers go through
|
|
25
|
+
* `toLocaleString("en-US")`, the way the canvas prints them.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ The arithmetic must agree with the canvas renderer's
|
|
28
|
+
* (`packages/renderer/src/node-types/array-mapper-pagination.ts`): a builder
|
|
29
|
+
* styling "Showing 1–8 of 12" must see the same words on the published page.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
// tslint:disable:no-any
|
|
33
|
+
|
|
34
|
+
export interface RangeControlVars {
|
|
35
|
+
/** `ds_N_total` — `null` until counted. */
|
|
36
|
+
totalVar: string
|
|
37
|
+
/** The page the list stands on: `ds_N_page`, or `ds_N_state.page` beside a search. */
|
|
38
|
+
currentPageExpr: types.Expression
|
|
39
|
+
perPage: number
|
|
40
|
+
/** An appending list always shows from the first row. */
|
|
41
|
+
infiniteScroll: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PLACEHOLDER = /\{(from|to|total)\}/g
|
|
45
|
+
|
|
46
|
+
const total = (vars: RangeControlVars): types.Identifier => types.identifier(vars.totalVar)
|
|
47
|
+
|
|
48
|
+
const page = (vars: RangeControlVars): types.Expression =>
|
|
49
|
+
types.cloneNode(vars.currentPageExpr, true) as types.Expression
|
|
50
|
+
|
|
51
|
+
/** `total === 0 ? 0 : <firstRow>` */
|
|
52
|
+
const unlessEmpty = (vars: RangeControlVars, firstRow: types.Expression): types.Expression =>
|
|
53
|
+
types.conditionalExpression(
|
|
54
|
+
types.binaryExpression('===', total(vars), types.numericLiteral(0)),
|
|
55
|
+
types.numericLiteral(0),
|
|
56
|
+
firstRow
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const buildFrom = (vars: RangeControlVars): types.Expression =>
|
|
60
|
+
unlessEmpty(
|
|
61
|
+
vars,
|
|
62
|
+
vars.infiniteScroll
|
|
63
|
+
? types.numericLiteral(1)
|
|
64
|
+
: types.binaryExpression(
|
|
65
|
+
'+',
|
|
66
|
+
types.binaryExpression(
|
|
67
|
+
'*',
|
|
68
|
+
types.binaryExpression('-', page(vars), types.numericLiteral(1)),
|
|
69
|
+
types.numericLiteral(vars.perPage)
|
|
70
|
+
),
|
|
71
|
+
types.numericLiteral(1)
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
/** `Math.min(page * perPage, total)` */
|
|
76
|
+
const buildTo = (vars: RangeControlVars): types.Expression =>
|
|
77
|
+
types.callExpression(types.memberExpression(types.identifier('Math'), types.identifier('min')), [
|
|
78
|
+
types.binaryExpression('*', page(vars), types.numericLiteral(vars.perPage)),
|
|
79
|
+
total(vars),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const buildNumber = (placeholder: string, vars: RangeControlVars): types.Expression => {
|
|
83
|
+
const value =
|
|
84
|
+
placeholder === 'from' ? buildFrom(vars) : placeholder === 'to' ? buildTo(vars) : total(vars)
|
|
85
|
+
return types.callExpression(types.memberExpression(value, types.identifier('toLocaleString')), [
|
|
86
|
+
types.stringLiteral('en-US'),
|
|
87
|
+
])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const templateElement = (text: string, tail: boolean): types.TemplateElement =>
|
|
91
|
+
types.templateElement(
|
|
92
|
+
{ raw: text.replace(/\\|`|\$\{/g, (match) => `\\${match}`), cooked: text },
|
|
93
|
+
tail
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The JSX printer writes a brace inside a static attribute as `{` /
|
|
98
|
+
* `}` so it is never read as an expression; the placeholders are braces,
|
|
99
|
+
* so the template is read back through that escaping.
|
|
100
|
+
*/
|
|
101
|
+
const decodeBraces = (text: string): string =>
|
|
102
|
+
text
|
|
103
|
+
.replace(/{|{|{/gi, '{')
|
|
104
|
+
.replace(/}|}|}/gi, '}')
|
|
105
|
+
.replace(/&/g, '&')
|
|
106
|
+
|
|
107
|
+
/** The author's template as a template literal, each placeholder a live number. */
|
|
108
|
+
const buildLiveText = (template: string, vars: RangeControlVars): types.TemplateLiteral => {
|
|
109
|
+
const quasis: types.TemplateElement[] = []
|
|
110
|
+
const expressions: types.Expression[] = []
|
|
111
|
+
const placeholder = new RegExp(PLACEHOLDER.source, 'g')
|
|
112
|
+
let last = 0
|
|
113
|
+
let match = placeholder.exec(template)
|
|
114
|
+
while (match) {
|
|
115
|
+
quasis.push(templateElement(template.slice(last, match.index), false))
|
|
116
|
+
expressions.push(buildNumber(match[1], vars))
|
|
117
|
+
last = match.index + match[0].length
|
|
118
|
+
match = placeholder.exec(template)
|
|
119
|
+
}
|
|
120
|
+
quasis.push(templateElement(template.slice(last), true))
|
|
121
|
+
return types.templateLiteral(quasis, expressions)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Every `range` control under a component body, in document order. The body is
|
|
126
|
+
* a `BlockStatement`, so the walk has to cross statements, ternaries and the
|
|
127
|
+
* `cond && <jsx/>` of a gated section, not only JSX children.
|
|
128
|
+
*/
|
|
129
|
+
export const findRangeControls = (root: any): any[] => {
|
|
130
|
+
const found: any[] = []
|
|
131
|
+
const traverse = (node: any): void => {
|
|
132
|
+
if (!node || typeof node !== 'object') {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
node.type === 'JSXElement' &&
|
|
137
|
+
readJSXAttributeString(node, PAGINATION_CONTROL_ATTR) === 'range'
|
|
138
|
+
) {
|
|
139
|
+
found.push(node)
|
|
140
|
+
}
|
|
141
|
+
if (Array.isArray(node.children)) {
|
|
142
|
+
node.children.forEach(traverse)
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(node.body)) {
|
|
145
|
+
node.body.forEach(traverse)
|
|
146
|
+
} else {
|
|
147
|
+
traverse(node.body)
|
|
148
|
+
}
|
|
149
|
+
traverse(node.consequent)
|
|
150
|
+
traverse(node.alternate)
|
|
151
|
+
traverse(node.left)
|
|
152
|
+
traverse(node.right)
|
|
153
|
+
traverse(node.test)
|
|
154
|
+
traverse(node.expression)
|
|
155
|
+
traverse(node.argument)
|
|
156
|
+
}
|
|
157
|
+
traverse(root)
|
|
158
|
+
return found
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The element's own children as one expression, so they can be the fallback of
|
|
163
|
+
* a ternary: plain text becomes a string, anything richer rides along in a
|
|
164
|
+
* fragment.
|
|
165
|
+
*/
|
|
166
|
+
const drawnText = (children: any[]): types.Expression => {
|
|
167
|
+
const texts: string[] = []
|
|
168
|
+
for (const child of children) {
|
|
169
|
+
if (child.type === 'JSXText') {
|
|
170
|
+
texts.push(child.value)
|
|
171
|
+
} else if (
|
|
172
|
+
child.type === 'JSXExpressionContainer' &&
|
|
173
|
+
child.expression?.type === 'StringLiteral'
|
|
174
|
+
) {
|
|
175
|
+
texts.push(child.expression.value)
|
|
176
|
+
} else {
|
|
177
|
+
return types.jsxFragment(types.jsxOpeningFragment(), types.jsxClosingFragment(), children)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return types.stringLiteral(texts.join('').replace(/\s+/g, ' ').trim())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `{total === null ? <drawn text> : `<template with live numbers>`}` in place of
|
|
185
|
+
* the element's children. The marker and template attributes stay on the
|
|
186
|
+
* element, so the published DOM carries the same contract the canvas reads.
|
|
187
|
+
*/
|
|
188
|
+
export const wireRangeControl = (node: any, vars: RangeControlVars): void => {
|
|
189
|
+
const template =
|
|
190
|
+
decodeBraces(readJSXAttributeString(node, RANGE_TEMPLATE_ATTR)) || DEFAULT_RANGE_TEMPLATE
|
|
191
|
+
const drawn = drawnText(Array.isArray(node.children) ? node.children : [])
|
|
192
|
+
node.children = [
|
|
193
|
+
types.jsxExpressionContainer(
|
|
194
|
+
types.conditionalExpression(
|
|
195
|
+
types.binaryExpression('===', total(vars), types.nullLiteral()),
|
|
196
|
+
drawn,
|
|
197
|
+
buildLiveText(template, vars)
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
]
|
|
201
|
+
if (node.openingElement) {
|
|
202
|
+
node.openingElement.selfClosing = false
|
|
203
|
+
}
|
|
204
|
+
if (!node.closingElement && node.openingElement?.name) {
|
|
205
|
+
node.closingElement = types.jsxClosingElement(types.cloneNode(node.openingElement.name, true))
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/row-order.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isListingTableWithId } from './list-sort-order'
|
|
2
2
|
|
|
3
3
|
interface ColumnLike {
|
|
4
4
|
name?: unknown
|
|
@@ -7,8 +7,9 @@ interface ColumnLike {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* The column that tells a table's rows apart, read from the table the project
|
|
10
|
-
* saved: its primary key, else a column named `id`. The
|
|
11
|
-
*
|
|
10
|
+
* saved: its primary key, else a column named `id`. The platform listing
|
|
11
|
+
* tables (products, blog posts, booking items) have `id` from their first
|
|
12
|
+
* migration. Null when neither is known — then nothing is added,
|
|
12
13
|
* rather than a column that may not exist.
|
|
13
14
|
*/
|
|
14
15
|
export const rowIdentityColumn = (
|
|
@@ -27,7 +28,7 @@ export const rowIdentityColumn = (
|
|
|
27
28
|
if (typeof key === 'string') {
|
|
28
29
|
return key
|
|
29
30
|
}
|
|
30
|
-
return
|
|
31
|
+
return isListingTableWithId(tableName) ? 'id' : null
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import { readJSXAttributeString } from './pagination-controls'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The search inputs of a page, and the mapper each one searches.
|
|
6
|
+
*
|
|
7
|
+
* Two kinds exist. The platform's own `DataSourceSearchInputNode` renders with
|
|
8
|
+
* a `search-input` class and is paired with a search-enabled mapper BY ORDER
|
|
9
|
+
* — the first such input searches the first such mapper. A page's OWN text
|
|
10
|
+
* input, kept where its author drew it, is marked `data-tq-search-for` with
|
|
11
|
+
* the node id of the mapper it searches — the marker the editor writes
|
|
12
|
+
* (`listing-search-field.ts`) and the canvas renderer binds by
|
|
13
|
+
* (`mapper-search-registry.ts`). Marked inputs are paired by that id and
|
|
14
|
+
* never take a place in the order, so a template's toolbar box beside a
|
|
15
|
+
* platform listing shifts nothing.
|
|
16
|
+
*
|
|
17
|
+
* Both are wired the same way: controlled by the mapper's search query state,
|
|
18
|
+
* which the mapper debounces into its fetch and keeps in the address.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const SEARCH_FOR_ATTR = 'data-tq-search-for'
|
|
22
|
+
|
|
23
|
+
// tslint:disable:no-any
|
|
24
|
+
|
|
25
|
+
export interface SearchInput {
|
|
26
|
+
node: any
|
|
27
|
+
/** The mapper node id a marked input names; undefined for a platform input paired by order. */
|
|
28
|
+
forNodeId?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isInput(node: any): boolean {
|
|
32
|
+
return node?.type === 'JSXElement' && node.openingElement?.name?.name === 'input'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasSearchInputClass(node: any): boolean {
|
|
36
|
+
return readJSXAttributeString(node, 'className').includes('search-input')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every search input under `root`, in document order. */
|
|
40
|
+
export function findAllSearchInputsInJSX(root: any): SearchInput[] {
|
|
41
|
+
const results: SearchInput[] = []
|
|
42
|
+
|
|
43
|
+
const traverse = (node: any): void => {
|
|
44
|
+
if (!node) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isInput(node)) {
|
|
49
|
+
const forNodeId = readJSXAttributeString(node, SEARCH_FOR_ATTR)
|
|
50
|
+
if (forNodeId) {
|
|
51
|
+
results.push({ node, forNodeId })
|
|
52
|
+
} else if (hasSearchInputClass(node)) {
|
|
53
|
+
results.push({ node })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (node.children && Array.isArray(node.children)) {
|
|
58
|
+
node.children.forEach((c: any) => traverse(c))
|
|
59
|
+
}
|
|
60
|
+
if (node.body) {
|
|
61
|
+
if (Array.isArray(node.body)) {
|
|
62
|
+
node.body.forEach((s: any) => traverse(s))
|
|
63
|
+
} else {
|
|
64
|
+
traverse(node.body)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (node.consequent) {
|
|
68
|
+
traverse(node.consequent)
|
|
69
|
+
}
|
|
70
|
+
if (node.alternate) {
|
|
71
|
+
traverse(node.alternate)
|
|
72
|
+
}
|
|
73
|
+
// `{activeTabIndex === 0 && (<div>…<DataProvider/>…</div>)}` — a tab
|
|
74
|
+
// panel, a gated section: the provider sits on the RIGHT of a logical
|
|
75
|
+
// expression (or in a ternary's `test`), which is not a child, a body or
|
|
76
|
+
// an argument. Left unvisited, the products list under the first tab of
|
|
77
|
+
// a shop was never paginated or seeded, and rendered nothing.
|
|
78
|
+
if (node.left) {
|
|
79
|
+
traverse(node.left)
|
|
80
|
+
}
|
|
81
|
+
if (node.right) {
|
|
82
|
+
traverse(node.right)
|
|
83
|
+
}
|
|
84
|
+
if (node.test) {
|
|
85
|
+
traverse(node.test)
|
|
86
|
+
}
|
|
87
|
+
if (node.expression) {
|
|
88
|
+
traverse(node.expression)
|
|
89
|
+
}
|
|
90
|
+
if (node.argument) {
|
|
91
|
+
traverse(node.argument)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
traverse(root)
|
|
96
|
+
return results
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The search-enabled usage each input searches: a marked input's by the mapper
|
|
101
|
+
* node id it names, a platform input's by its place among the platform inputs.
|
|
102
|
+
*/
|
|
103
|
+
export function pairSearchInputs<Usage extends { searchEnabled: boolean; nodeId?: string }>(
|
|
104
|
+
inputs: SearchInput[],
|
|
105
|
+
usages: Usage[]
|
|
106
|
+
): Map<any, Usage> {
|
|
107
|
+
const searchable = usages.filter((usage) => usage.searchEnabled)
|
|
108
|
+
const paired = new Map<any, Usage>()
|
|
109
|
+
let position = 0
|
|
110
|
+
for (const input of inputs) {
|
|
111
|
+
if (input.forNodeId) {
|
|
112
|
+
const named = searchable.find((candidate) => candidate.nodeId === input.forNodeId)
|
|
113
|
+
if (named) {
|
|
114
|
+
paired.set(input.node, named)
|
|
115
|
+
}
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
const next = searchable[position]
|
|
119
|
+
position += 1
|
|
120
|
+
if (next) {
|
|
121
|
+
paired.set(input.node, next)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return paired
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Controls the input by the mapper's query: `value` reads it, `onChange`
|
|
129
|
+
* writes it. A marked input may sit inside a form of the page's own; Enter
|
|
130
|
+
* in it must not submit that form and reload the filtered list away.
|
|
131
|
+
*/
|
|
132
|
+
export function wireSearchInput(
|
|
133
|
+
input: SearchInput,
|
|
134
|
+
vars: { searchQueryVar: string; setSearchQueryVar: string }
|
|
135
|
+
): void {
|
|
136
|
+
const attributes = input.node.openingElement.attributes.filter(
|
|
137
|
+
(attr: any) => !['onChange', 'value', 'onKeyDown'].includes(attr.name?.name)
|
|
138
|
+
)
|
|
139
|
+
attributes.push(
|
|
140
|
+
types.jsxAttribute(
|
|
141
|
+
types.jsxIdentifier('onChange'),
|
|
142
|
+
types.jsxExpressionContainer(
|
|
143
|
+
types.arrowFunctionExpression(
|
|
144
|
+
[types.identifier('e')],
|
|
145
|
+
types.callExpression(types.identifier(vars.setSearchQueryVar), [
|
|
146
|
+
types.memberExpression(
|
|
147
|
+
types.memberExpression(types.identifier('e'), types.identifier('target')),
|
|
148
|
+
types.identifier('value')
|
|
149
|
+
),
|
|
150
|
+
])
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
types.jsxAttribute(
|
|
155
|
+
types.jsxIdentifier('value'),
|
|
156
|
+
types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
if (input.forNodeId) {
|
|
160
|
+
attributes.push(
|
|
161
|
+
types.jsxAttribute(
|
|
162
|
+
types.jsxIdentifier('onKeyDown'),
|
|
163
|
+
types.jsxExpressionContainer(
|
|
164
|
+
types.arrowFunctionExpression(
|
|
165
|
+
[types.identifier('e')],
|
|
166
|
+
types.blockStatement([
|
|
167
|
+
types.ifStatement(
|
|
168
|
+
types.binaryExpression(
|
|
169
|
+
'===',
|
|
170
|
+
types.memberExpression(types.identifier('e'), types.identifier('key')),
|
|
171
|
+
types.stringLiteral('Enter')
|
|
172
|
+
),
|
|
173
|
+
types.expressionStatement(
|
|
174
|
+
types.callExpression(
|
|
175
|
+
types.memberExpression(
|
|
176
|
+
types.identifier('e'),
|
|
177
|
+
types.identifier('preventDefault')
|
|
178
|
+
),
|
|
179
|
+
[]
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
),
|
|
183
|
+
])
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
input.node.openingElement.attributes = attributes
|
|
190
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
|
+
import { generateCategoryTaxonomyCode } from './category-taxonomy'
|
|
3
|
+
|
|
4
|
+
export interface BookingItemTransformOptions {
|
|
5
|
+
/** The booking items' category taxonomy — see `bookingSettings.categories`. */
|
|
6
|
+
categories?: UIDLEcommerceCategory[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates JavaScript code for the `teleport_booking_items` row
|
|
11
|
+
* transformation — the things a Bookings site lets people book.
|
|
12
|
+
*
|
|
13
|
+
* The row is kept whole (every column, snake_case: the booking pages bind
|
|
14
|
+
* them as stored) and its translatable texts — `name`, `description` (rich
|
|
15
|
+
* text) and `location` — are the page's language when the row has them there
|
|
16
|
+
* (`es_name`, …), the main language otherwise. Its `category` is the name its
|
|
17
|
+
* first category has in the page's language, read off the taxonomy baked in
|
|
18
|
+
* here (the row's stored main name while none of its ids is a category). That
|
|
19
|
+
* is all a booking page needs to show an item in the visitor's language; the
|
|
20
|
+
* booking statements localise what they read on their own.
|
|
21
|
+
*
|
|
22
|
+
* MUST mirror `localizeBookingItem` in the editor
|
|
23
|
+
* (teleport-gui features/bookings/utils/localize-booking-item.ts), which is
|
|
24
|
+
* what the canvas renders from.
|
|
25
|
+
*/
|
|
26
|
+
export const generateBookingItemTransformationCode = (
|
|
27
|
+
options: BookingItemTransformOptions = {}
|
|
28
|
+
): string => {
|
|
29
|
+
return `
|
|
30
|
+
// ============================================================
|
|
31
|
+
// Booking Item Transformation
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
var BOOKING_ITEM_TRANSLATABLE_COLUMNS = ['name', 'description', 'location']
|
|
35
|
+
${generateCategoryTaxonomyCode('BOOKING_CATEGORIES_BY_ID', options.categories)}
|
|
36
|
+
|
|
37
|
+
function buildBookingItem(record, options) {
|
|
38
|
+
if (!record || typeof record !== 'object') return record
|
|
39
|
+
options = options || {}
|
|
40
|
+
var currentLang = options.currentLanguage || null
|
|
41
|
+
var mainLang = options.mainLanguage || null
|
|
42
|
+
|
|
43
|
+
var out = {}
|
|
44
|
+
for (var key in record) {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) out[key] = record[key]
|
|
46
|
+
}
|
|
47
|
+
for (var i = 0; i < BOOKING_ITEM_TRANSLATABLE_COLUMNS.length; i++) {
|
|
48
|
+
var column = BOOKING_ITEM_TRANSLATABLE_COLUMNS[i]
|
|
49
|
+
var value = resolveI18nField(record, column, column, currentLang, mainLang)
|
|
50
|
+
out[column] = value === null || value === undefined ? record[column] : value
|
|
51
|
+
}
|
|
52
|
+
if ('category' in record || 'category_ids' in record) {
|
|
53
|
+
var stored = typeof record.category === 'string' ? record.category : ''
|
|
54
|
+
var assigned = resolveAssignedCategories(
|
|
55
|
+
record.category_ids,
|
|
56
|
+
BOOKING_CATEGORIES_BY_ID,
|
|
57
|
+
currentLang,
|
|
58
|
+
mainLang,
|
|
59
|
+
stored
|
|
60
|
+
)
|
|
61
|
+
out.category = assigned.length > 0 ? assigned[0].name : stored
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function transformBookingItems(records, options) {
|
|
67
|
+
if (!Array.isArray(records)) return []
|
|
68
|
+
return records.map(function(record) { return buildBookingItem(record, options) })
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
}
|
|
@@ -8,6 +8,7 @@ import { ContentTables, StorefrontTax, TableAccess } from '@teleporthq/teleport-
|
|
|
8
8
|
import { generateSharedTransformationCode } from './shared-utils'
|
|
9
9
|
import { generateBlogPostTransformationCode } from './blog-post'
|
|
10
10
|
import { generateCustomPageTransformationCode } from './custom-page'
|
|
11
|
+
import { generateBookingItemTransformationCode } from './booking-item'
|
|
11
12
|
import {
|
|
12
13
|
generateEcommerceProductTransformationCode,
|
|
13
14
|
type EcommerceProductTransformOptions,
|
|
@@ -45,6 +46,8 @@ export interface EntityTransformOptions extends EcommerceProductTransformOptions
|
|
|
45
46
|
contentSettings?: Partial<Record<ContentTables.ContentPresetKey, ContentPresetTransformSettings>>
|
|
46
47
|
/** Blog post-category taxonomy — see `blogSettings.categories`. */
|
|
47
48
|
blogCategories?: UIDLEcommerceCategory[]
|
|
49
|
+
/** The booking items' category taxonomy — see `bookingSettings.categories`. */
|
|
50
|
+
bookingCategories?: UIDLEcommerceCategory[]
|
|
48
51
|
/** Post content headings get ids and `#` links — see `blogSettings.headingAnchors`. */
|
|
49
52
|
blogHeadingAnchors?: boolean
|
|
50
53
|
/**
|
|
@@ -136,7 +139,12 @@ export const contentSettingsFor = (
|
|
|
136
139
|
export const buildProductTransformOptions = (
|
|
137
140
|
options: Pick<
|
|
138
141
|
GeneratorOptions,
|
|
139
|
-
|
|
142
|
+
| 'ecommerceSettings'
|
|
143
|
+
| 'invoiceSettings'
|
|
144
|
+
| 'blogSettings'
|
|
145
|
+
| 'bookingSettings'
|
|
146
|
+
| 'helpCenterSettings'
|
|
147
|
+
| 'auth'
|
|
140
148
|
> &
|
|
141
149
|
LocalizedProjectOptions & {
|
|
142
150
|
/**
|
|
@@ -157,6 +165,7 @@ export const buildProductTransformOptions = (
|
|
|
157
165
|
help: contentPresetTransformSettings(options.helpCenterSettings),
|
|
158
166
|
},
|
|
159
167
|
blogCategories: options.blogSettings?.categories,
|
|
168
|
+
bookingCategories: options.bookingSettings?.categories,
|
|
160
169
|
blogHeadingAnchors: options.blogSettings?.headingAnchors === true,
|
|
161
170
|
blogComments: options.blogSettings?.comments === true,
|
|
162
171
|
storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
|
|
@@ -166,7 +175,12 @@ export const buildProductTransformOptions = (
|
|
|
166
175
|
trustedReaderRoles: TableAccess.resolveTrustedReaderRoles(options.auth || options.authentication),
|
|
167
176
|
})
|
|
168
177
|
|
|
169
|
-
export type TransformationType =
|
|
178
|
+
export type TransformationType =
|
|
179
|
+
| 'blog-post'
|
|
180
|
+
| 'ecommerce-product'
|
|
181
|
+
| 'custom-page'
|
|
182
|
+
| 'booking-item'
|
|
183
|
+
| null
|
|
170
184
|
|
|
171
185
|
/**
|
|
172
186
|
* Strips a leading `schema.` qualifier from a table name so that only the
|
|
@@ -207,6 +221,9 @@ export const detectTransformationType = (tableName: string): TransformationType
|
|
|
207
221
|
if (bare === 'teleport_pages') {
|
|
208
222
|
return 'custom-page'
|
|
209
223
|
}
|
|
224
|
+
if (bare === 'teleport_booking_items') {
|
|
225
|
+
return 'booking-item'
|
|
226
|
+
}
|
|
210
227
|
return null
|
|
211
228
|
}
|
|
212
229
|
|
|
@@ -245,6 +262,10 @@ export const getTransformationCode = (
|
|
|
245
262
|
return shared + generateEcommerceProductTransformationCode(options)
|
|
246
263
|
case 'custom-page':
|
|
247
264
|
return shared + generateCustomPageTransformationCode()
|
|
265
|
+
case 'booking-item':
|
|
266
|
+
return (
|
|
267
|
+
shared + generateBookingItemTransformationCode({ categories: options.bookingCategories })
|
|
268
|
+
)
|
|
248
269
|
default:
|
|
249
270
|
return ''
|
|
250
271
|
}
|
|
@@ -380,6 +401,8 @@ export const getTransformWrapperCode = (
|
|
|
380
401
|
? 'transformBlogPosts'
|
|
381
402
|
: type === 'custom-page'
|
|
382
403
|
? 'transformCustomPages'
|
|
404
|
+
: type === 'booking-item'
|
|
405
|
+
? 'transformBookingItems'
|
|
383
406
|
: 'transformEcommerceProducts'
|
|
384
407
|
|
|
385
408
|
// Products additionally get their purchasable variant combinations attached in
|