@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.
Files changed (136) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +87 -4
  2. package/__tests__/booking-item-transform.test.ts +120 -0
  3. package/__tests__/browser-row-policy.test.ts +8 -2
  4. package/__tests__/fetchers.test.ts +1 -1
  5. package/__tests__/fixed-list-sort.test.ts +1 -1
  6. package/__tests__/infinite-scroll.test.ts +154 -5
  7. package/__tests__/list-sort-order.test.ts +61 -0
  8. package/__tests__/null-filter-where.test.ts +132 -0
  9. package/__tests__/pg-date-parser.test.ts +73 -0
  10. package/__tests__/plain-list-sort.test.ts +1 -1
  11. package/__tests__/product-price-sort.ts +11 -6
  12. package/__tests__/range-control.test.ts +159 -0
  13. package/__tests__/search-input-marker.test.ts +86 -0
  14. package/__tests__/search-url-sync.test.ts +34 -8
  15. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  16. package/dist/cjs/count-effect.d.ts +7 -1
  17. package/dist/cjs/count-effect.d.ts.map +1 -1
  18. package/dist/cjs/count-effect.js +15 -5
  19. package/dist/cjs/count-effect.js.map +1 -1
  20. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  21. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  22. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  23. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  24. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  25. package/dist/cjs/fetchers/postgresql.js +3 -1
  26. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  27. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  28. package/dist/cjs/fetchers/raw-query.js +2 -1
  29. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  30. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  31. package/dist/cjs/fetchers/teleport.js +4 -2
  32. package/dist/cjs/fetchers/teleport.js.map +1 -1
  33. package/dist/cjs/infinite-scroll.d.ts +25 -2
  34. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  35. package/dist/cjs/infinite-scroll.js +144 -33
  36. package/dist/cjs/infinite-scroll.js.map +1 -1
  37. package/dist/cjs/list-sort-order.d.ts +14 -0
  38. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  39. package/dist/cjs/list-sort-order.js +36 -0
  40. package/dist/cjs/list-sort-order.js.map +1 -0
  41. package/dist/cjs/pagination-controls.d.ts +22 -3
  42. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  43. package/dist/cjs/pagination-controls.js +29 -3
  44. package/dist/cjs/pagination-controls.js.map +1 -1
  45. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  46. package/dist/cjs/pagination-plugin.js +137 -101
  47. package/dist/cjs/pagination-plugin.js.map +1 -1
  48. package/dist/cjs/range-control.d.ts +45 -0
  49. package/dist/cjs/range-control.d.ts.map +1 -0
  50. package/dist/cjs/range-control.js +165 -0
  51. package/dist/cjs/range-control.js.map +1 -0
  52. package/dist/cjs/row-order.d.ts +3 -2
  53. package/dist/cjs/row-order.d.ts.map +1 -1
  54. package/dist/cjs/row-order.js +5 -4
  55. package/dist/cjs/row-order.js.map +1 -1
  56. package/dist/cjs/search-input.d.ts +42 -0
  57. package/dist/cjs/search-input.d.ts.map +1 -0
  58. package/dist/cjs/search-input.js +158 -0
  59. package/dist/cjs/search-input.js.map +1 -0
  60. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  61. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  62. package/dist/cjs/transformations/booking-item.js +27 -0
  63. package/dist/cjs/transformations/booking-item.js.map +1 -0
  64. package/dist/cjs/transformations/index.d.ts +4 -2
  65. package/dist/cjs/transformations/index.d.ts.map +1 -1
  66. package/dist/cjs/transformations/index.js +14 -5
  67. package/dist/cjs/transformations/index.js.map +1 -1
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/esm/count-effect.d.ts +7 -1
  70. package/dist/esm/count-effect.d.ts.map +1 -1
  71. package/dist/esm/count-effect.js +15 -5
  72. package/dist/esm/count-effect.js.map +1 -1
  73. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  74. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  75. package/dist/esm/fetchers/pg-client-import.js +12 -0
  76. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  77. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  78. package/dist/esm/fetchers/postgresql.js +3 -1
  79. package/dist/esm/fetchers/postgresql.js.map +1 -1
  80. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  81. package/dist/esm/fetchers/raw-query.js +2 -1
  82. package/dist/esm/fetchers/raw-query.js.map +1 -1
  83. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  84. package/dist/esm/fetchers/teleport.js +4 -2
  85. package/dist/esm/fetchers/teleport.js.map +1 -1
  86. package/dist/esm/infinite-scroll.d.ts +25 -2
  87. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  88. package/dist/esm/infinite-scroll.js +142 -32
  89. package/dist/esm/infinite-scroll.js.map +1 -1
  90. package/dist/esm/list-sort-order.d.ts +14 -0
  91. package/dist/esm/list-sort-order.d.ts.map +1 -0
  92. package/dist/esm/list-sort-order.js +32 -0
  93. package/dist/esm/list-sort-order.js.map +1 -0
  94. package/dist/esm/pagination-controls.d.ts +22 -3
  95. package/dist/esm/pagination-controls.d.ts.map +1 -1
  96. package/dist/esm/pagination-controls.js +28 -2
  97. package/dist/esm/pagination-controls.js.map +1 -1
  98. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  99. package/dist/esm/pagination-plugin.js +138 -102
  100. package/dist/esm/pagination-plugin.js.map +1 -1
  101. package/dist/esm/range-control.d.ts +45 -0
  102. package/dist/esm/range-control.d.ts.map +1 -0
  103. package/dist/esm/range-control.js +137 -0
  104. package/dist/esm/range-control.js.map +1 -0
  105. package/dist/esm/row-order.d.ts +3 -2
  106. package/dist/esm/row-order.d.ts.map +1 -1
  107. package/dist/esm/row-order.js +5 -4
  108. package/dist/esm/row-order.js.map +1 -1
  109. package/dist/esm/search-input.d.ts +42 -0
  110. package/dist/esm/search-input.d.ts.map +1 -0
  111. package/dist/esm/search-input.js +129 -0
  112. package/dist/esm/search-input.js.map +1 -0
  113. package/dist/esm/transformations/booking-item.d.ts +24 -0
  114. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  115. package/dist/esm/transformations/booking-item.js +23 -0
  116. package/dist/esm/transformations/booking-item.js.map +1 -0
  117. package/dist/esm/transformations/index.d.ts +4 -2
  118. package/dist/esm/transformations/index.d.ts.map +1 -1
  119. package/dist/esm/transformations/index.js +14 -5
  120. package/dist/esm/transformations/index.js.map +1 -1
  121. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +5 -5
  123. package/src/count-effect.ts +31 -8
  124. package/src/fetchers/pg-client-import.ts +16 -0
  125. package/src/fetchers/postgresql.ts +15 -9
  126. package/src/fetchers/raw-query.ts +2 -1
  127. package/src/fetchers/teleport.ts +11 -8
  128. package/src/infinite-scroll.ts +240 -79
  129. package/src/list-sort-order.ts +34 -0
  130. package/src/pagination-controls.ts +26 -3
  131. package/src/pagination-plugin.ts +163 -135
  132. package/src/range-control.ts +207 -0
  133. package/src/row-order.ts +5 -4
  134. package/src/search-input.ts +190 -0
  135. package/src/transformations/booking-item.ts +71 -0
  136. 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 `&#123;` /
98
+ * `&#125;` 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(/&#123;|&#x7b;|&lbrace;/gi, '{')
104
+ .replace(/&#125;|&#x7d;|&rbrace;/gi, '}')
105
+ .replace(/&amp;/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 { supportsProductPriceSort } from './product-price-sort'
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 products table has `id`
11
- * from its first migration. Null when neither is known — then nothing is added,
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 supportsProductPriceSort(tableName) ? 'id' : null
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
- 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'helpCenterSettings' | 'auth'
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 = 'blog-post' | 'ecommerce-product' | 'custom-page' | null
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