@teleporthq/teleport-plugin-next-data-source 0.43.74 → 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 (231) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +95 -5
  2. package/__tests__/blog-adjacent-arranged.test.ts +176 -0
  3. package/__tests__/blog-post-contents.test.ts +212 -0
  4. package/__tests__/blog-post-page-enrichment.test.ts +28 -5
  5. package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
  6. package/__tests__/blog-post-reading-time.test.ts +55 -0
  7. package/__tests__/booking-item-transform.test.ts +120 -0
  8. package/__tests__/browser-row-policy.test.ts +13 -3
  9. package/__tests__/content-preset-wiring.test.ts +108 -0
  10. package/__tests__/fetchers.test.ts +1 -1
  11. package/__tests__/fixed-list-sort.test.ts +183 -0
  12. package/__tests__/infinite-scroll.test.ts +154 -5
  13. package/__tests__/list-sort-order.test.ts +61 -0
  14. package/__tests__/null-filter-where.test.ts +132 -0
  15. package/__tests__/pagination-filter-groups.test.ts +186 -0
  16. package/__tests__/pg-date-parser.test.ts +73 -0
  17. package/__tests__/plain-list-sort.test.ts +215 -0
  18. package/__tests__/product-price-sort.ts +58 -25
  19. package/__tests__/range-control.test.ts +159 -0
  20. package/__tests__/rest-api-key-header.test.ts +18 -0
  21. package/__tests__/search-input-marker.test.ts +86 -0
  22. package/__tests__/search-url-sync.test.ts +61 -16
  23. package/__tests__/shared-resource-prefetch.test.ts +65 -0
  24. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  25. package/dist/cjs/count-effect.d.ts +7 -1
  26. package/dist/cjs/count-effect.d.ts.map +1 -1
  27. package/dist/cjs/count-effect.js +15 -5
  28. package/dist/cjs/count-effect.js.map +1 -1
  29. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  30. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  31. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  32. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  33. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/postgresql.js +4 -1
  35. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  36. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/raw-query.js +2 -1
  38. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  39. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/rest-api.js +3 -1
  41. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  42. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/teleport.js +5 -2
  44. package/dist/cjs/fetchers/teleport.js.map +1 -1
  45. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/filter-utils.d.ts +19 -7
  50. package/dist/cjs/filter-utils.d.ts.map +1 -1
  51. package/dist/cjs/filter-utils.js +49 -13
  52. package/dist/cjs/filter-utils.js.map +1 -1
  53. package/dist/cjs/infinite-scroll.d.ts +25 -2
  54. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  55. package/dist/cjs/infinite-scroll.js +144 -33
  56. package/dist/cjs/infinite-scroll.js.map +1 -1
  57. package/dist/cjs/list-sort-order.d.ts +14 -0
  58. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  59. package/dist/cjs/list-sort-order.js +36 -0
  60. package/dist/cjs/list-sort-order.js.map +1 -0
  61. package/dist/cjs/pagination-controls.d.ts +22 -3
  62. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  63. package/dist/cjs/pagination-controls.js +29 -3
  64. package/dist/cjs/pagination-controls.js.map +1 -1
  65. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  66. package/dist/cjs/pagination-plugin.js +267 -212
  67. package/dist/cjs/pagination-plugin.js.map +1 -1
  68. package/dist/cjs/product-price-sort.d.ts +0 -9
  69. package/dist/cjs/product-price-sort.d.ts.map +1 -1
  70. package/dist/cjs/product-price-sort.js +1 -13
  71. package/dist/cjs/product-price-sort.js.map +1 -1
  72. package/dist/cjs/range-control.d.ts +45 -0
  73. package/dist/cjs/range-control.d.ts.map +1 -0
  74. package/dist/cjs/range-control.js +165 -0
  75. package/dist/cjs/range-control.js.map +1 -0
  76. package/dist/cjs/row-order.d.ts +20 -0
  77. package/dist/cjs/row-order.d.ts.map +1 -0
  78. package/dist/cjs/row-order.js +46 -0
  79. package/dist/cjs/row-order.js.map +1 -0
  80. package/dist/cjs/search-input.d.ts +42 -0
  81. package/dist/cjs/search-input.d.ts.map +1 -0
  82. package/dist/cjs/search-input.js +158 -0
  83. package/dist/cjs/search-input.js.map +1 -0
  84. package/dist/cjs/sort-utils.d.ts +10 -0
  85. package/dist/cjs/sort-utils.d.ts.map +1 -1
  86. package/dist/cjs/sort-utils.js +22 -1
  87. package/dist/cjs/sort-utils.js.map +1 -1
  88. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
  89. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
  90. package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
  91. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
  92. package/dist/cjs/transformations/blog-comments.d.ts +1 -1
  93. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
  94. package/dist/cjs/transformations/blog-comments.js +1 -1
  95. package/dist/cjs/transformations/blog-comments.js.map +1 -1
  96. package/dist/cjs/transformations/blog-post.d.ts +14 -0
  97. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  98. package/dist/cjs/transformations/blog-post.js +4 -1
  99. package/dist/cjs/transformations/blog-post.js.map +1 -1
  100. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  101. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  102. package/dist/cjs/transformations/booking-item.js +27 -0
  103. package/dist/cjs/transformations/booking-item.js.map +1 -0
  104. package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
  105. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
  106. package/dist/cjs/transformations/heading-anchors.js +34 -10
  107. package/dist/cjs/transformations/heading-anchors.js.map +1 -1
  108. package/dist/cjs/transformations/index.d.ts +27 -13
  109. package/dist/cjs/transformations/index.d.ts.map +1 -1
  110. package/dist/cjs/transformations/index.js +73 -16
  111. package/dist/cjs/transformations/index.js.map +1 -1
  112. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  113. package/dist/cjs/utils.d.ts.map +1 -1
  114. package/dist/cjs/utils.js +52 -3
  115. package/dist/cjs/utils.js.map +1 -1
  116. package/dist/esm/count-effect.d.ts +7 -1
  117. package/dist/esm/count-effect.d.ts.map +1 -1
  118. package/dist/esm/count-effect.js +15 -5
  119. package/dist/esm/count-effect.js.map +1 -1
  120. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  121. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  122. package/dist/esm/fetchers/pg-client-import.js +12 -0
  123. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  124. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  125. package/dist/esm/fetchers/postgresql.js +5 -2
  126. package/dist/esm/fetchers/postgresql.js.map +1 -1
  127. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  128. package/dist/esm/fetchers/raw-query.js +2 -1
  129. package/dist/esm/fetchers/raw-query.js.map +1 -1
  130. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  131. package/dist/esm/fetchers/rest-api.js +3 -1
  132. package/dist/esm/fetchers/rest-api.js.map +1 -1
  133. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  134. package/dist/esm/fetchers/teleport.js +6 -3
  135. package/dist/esm/fetchers/teleport.js.map +1 -1
  136. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
  137. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  138. package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
  139. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  140. package/dist/esm/filter-utils.d.ts +19 -7
  141. package/dist/esm/filter-utils.d.ts.map +1 -1
  142. package/dist/esm/filter-utils.js +46 -12
  143. package/dist/esm/filter-utils.js.map +1 -1
  144. package/dist/esm/infinite-scroll.d.ts +25 -2
  145. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  146. package/dist/esm/infinite-scroll.js +142 -32
  147. package/dist/esm/infinite-scroll.js.map +1 -1
  148. package/dist/esm/list-sort-order.d.ts +14 -0
  149. package/dist/esm/list-sort-order.d.ts.map +1 -0
  150. package/dist/esm/list-sort-order.js +32 -0
  151. package/dist/esm/list-sort-order.js.map +1 -0
  152. package/dist/esm/pagination-controls.d.ts +22 -3
  153. package/dist/esm/pagination-controls.d.ts.map +1 -1
  154. package/dist/esm/pagination-controls.js +28 -2
  155. package/dist/esm/pagination-controls.js.map +1 -1
  156. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  157. package/dist/esm/pagination-plugin.js +271 -216
  158. package/dist/esm/pagination-plugin.js.map +1 -1
  159. package/dist/esm/product-price-sort.d.ts +0 -9
  160. package/dist/esm/product-price-sort.d.ts.map +1 -1
  161. package/dist/esm/product-price-sort.js +0 -11
  162. package/dist/esm/product-price-sort.js.map +1 -1
  163. package/dist/esm/range-control.d.ts +45 -0
  164. package/dist/esm/range-control.d.ts.map +1 -0
  165. package/dist/esm/range-control.js +137 -0
  166. package/dist/esm/range-control.js.map +1 -0
  167. package/dist/esm/row-order.d.ts +20 -0
  168. package/dist/esm/row-order.d.ts.map +1 -0
  169. package/dist/esm/row-order.js +41 -0
  170. package/dist/esm/row-order.js.map +1 -0
  171. package/dist/esm/search-input.d.ts +42 -0
  172. package/dist/esm/search-input.d.ts.map +1 -0
  173. package/dist/esm/search-input.js +129 -0
  174. package/dist/esm/search-input.js.map +1 -0
  175. package/dist/esm/sort-utils.d.ts +10 -0
  176. package/dist/esm/sort-utils.d.ts.map +1 -1
  177. package/dist/esm/sort-utils.js +20 -0
  178. package/dist/esm/sort-utils.js.map +1 -1
  179. package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
  180. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
  181. package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
  182. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
  183. package/dist/esm/transformations/blog-comments.d.ts +1 -1
  184. package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
  185. package/dist/esm/transformations/blog-comments.js +1 -1
  186. package/dist/esm/transformations/blog-comments.js.map +1 -1
  187. package/dist/esm/transformations/blog-post.d.ts +14 -0
  188. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  189. package/dist/esm/transformations/blog-post.js +4 -1
  190. package/dist/esm/transformations/blog-post.js.map +1 -1
  191. package/dist/esm/transformations/booking-item.d.ts +24 -0
  192. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  193. package/dist/esm/transformations/booking-item.js +23 -0
  194. package/dist/esm/transformations/booking-item.js.map +1 -0
  195. package/dist/esm/transformations/heading-anchors.d.ts +32 -8
  196. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
  197. package/dist/esm/transformations/heading-anchors.js +33 -9
  198. package/dist/esm/transformations/heading-anchors.js.map +1 -1
  199. package/dist/esm/transformations/index.d.ts +27 -13
  200. package/dist/esm/transformations/index.d.ts.map +1 -1
  201. package/dist/esm/transformations/index.js +72 -16
  202. package/dist/esm/transformations/index.js.map +1 -1
  203. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  204. package/dist/esm/utils.d.ts.map +1 -1
  205. package/dist/esm/utils.js +52 -3
  206. package/dist/esm/utils.js.map +1 -1
  207. package/package.json +5 -5
  208. package/src/count-effect.ts +31 -8
  209. package/src/fetchers/pg-client-import.ts +16 -0
  210. package/src/fetchers/postgresql.ts +25 -23
  211. package/src/fetchers/raw-query.ts +2 -1
  212. package/src/fetchers/rest-api.ts +3 -1
  213. package/src/fetchers/teleport.ts +22 -23
  214. package/src/fetchers/utils/browser-row-policy.ts +10 -20
  215. package/src/filter-utils.ts +102 -28
  216. package/src/infinite-scroll.ts +240 -79
  217. package/src/list-sort-order.ts +34 -0
  218. package/src/pagination-controls.ts +26 -3
  219. package/src/pagination-plugin.ts +370 -306
  220. package/src/product-price-sort.ts +0 -11
  221. package/src/range-control.ts +207 -0
  222. package/src/row-order.ts +55 -0
  223. package/src/search-input.ts +190 -0
  224. package/src/sort-utils.ts +25 -0
  225. package/src/transformations/blog-adjacent-posts.ts +89 -3
  226. package/src/transformations/blog-comments.ts +5 -5
  227. package/src/transformations/blog-post.ts +64 -13
  228. package/src/transformations/booking-item.ts +71 -0
  229. package/src/transformations/heading-anchors.ts +79 -16
  230. package/src/transformations/index.ts +104 -14
  231. package/src/utils.ts +74 -3
@@ -191,14 +191,3 @@ export const generateSortFallbackFieldHelper = (tableName: string): string => {
191
191
 
192
192
  return ['function sortFallbackField(field) {', branches + ' return field', '}'].join('\n')
193
193
  }
194
-
195
- /**
196
- * A deterministic tiebreaker appended to every products ORDER BY.
197
- *
198
- * Without one, a paginated sort with ties — and "sort by discount" ties every
199
- * undiscounted product at zero — has no defined order between equal rows, so
200
- * the same product can appear on two pages or on none. `id` is on the products
201
- * table from its first migration.
202
- */
203
- export const generateSortTiebreakSql = (tableName: string): string =>
204
- supportsProductPriceSort(tableName) ? ', "id" ASC' : ''
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ import { isListingTableWithId } from './list-sort-order'
2
+
3
+ interface ColumnLike {
4
+ name?: unknown
5
+ isPrimaryKey?: unknown
6
+ }
7
+
8
+ /**
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 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,
13
+ * rather than a column that may not exist.
14
+ */
15
+ export const rowIdentityColumn = (
16
+ config: Record<string, unknown>,
17
+ tableName: string
18
+ ): string | null => {
19
+ const selectedTables = (config as { selectedTables?: Record<string, { columns?: unknown }> })
20
+ .selectedTables
21
+ const saved = selectedTables?.[tableName]?.columns
22
+ const columns = (Array.isArray(saved) ? (saved as ColumnLike[]) : []).filter(
23
+ (column) => typeof column?.name === 'string'
24
+ )
25
+ const key =
26
+ columns.find((column) => column.isPrimaryKey === true)?.name ??
27
+ columns.find((column) => column.name === 'id')?.name
28
+ if (typeof key === 'string') {
29
+ return key
30
+ }
31
+ return isListingTableWithId(tableName) ? 'id' : null
32
+ }
33
+
34
+ /**
35
+ * The `rowOrderTiebreak(fields, direction)` helper a SQL fetcher embeds: what
36
+ * ends an ORDER BY so that it orders every row.
37
+ *
38
+ * Rows that share every requested key have no order of their own, and the
39
+ * database deals them out differently for each page — "sort by discount" ties
40
+ * every undiscounted product at zero, and an import dates hundreds of articles
41
+ * alike — so a paginated list repeated some rows and never showed others.
42
+ * The tie follows the last key's direction, as the editor's canvas does.
43
+ */
44
+ export const generateRowOrderTiebreakHelper = (identityColumn: string | null): string => {
45
+ if (!identityColumn) {
46
+ return "const rowOrderTiebreak = () => ''"
47
+ }
48
+ const quoted = `"${identityColumn.replace(/"/g, '""')}"`
49
+ return [
50
+ 'const rowOrderTiebreak = (fields, direction) =>',
51
+ ` fields.includes(${JSON.stringify(identityColumn)}) ? '' : ${JSON.stringify(
52
+ `, ${quoted} `
53
+ )} + direction`,
54
+ ].join('\n')
55
+ }
@@ -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
+ }
package/src/sort-utils.ts CHANGED
@@ -63,6 +63,31 @@ const toExpressionAndDeps = (
63
63
  return { expr: parsed, deps: Array.from(deps) }
64
64
  }
65
65
 
66
+ /**
67
+ * The `{ field, order }` of a sort the UIDL FIXES — a static column and a
68
+ * static direction (or none: ascending) — or undefined for one bound to state.
69
+ * A fixed sort is a static sort: it rides on the server-side first-page fetch
70
+ * and the client keeps that page, which a dynamic sort cannot let it do.
71
+ */
72
+ export const readFixedSort = (
73
+ sort: UIDLStaticValue | UIDLExpressionValue | undefined,
74
+ sortDirection: UIDLStaticValue | UIDLExpressionValue | undefined
75
+ ): { field: string; order: 'asc' | 'desc' } | undefined => {
76
+ if (sort?.type !== 'static' || typeof sort.content !== 'string' || !sort.content.trim()) {
77
+ return undefined
78
+ }
79
+ if (
80
+ sortDirection &&
81
+ (sortDirection.type !== 'static' || typeof sortDirection.content !== 'string')
82
+ ) {
83
+ return undefined
84
+ }
85
+ const direction = String(sortDirection?.content ?? '')
86
+ .trim()
87
+ .toLowerCase()
88
+ return { field: sort.content.trim(), order: direction.startsWith('desc') ? 'desc' : 'asc' }
89
+ }
90
+
66
91
  export const extractDynamicSort = (
67
92
  sort: UIDLStaticValue | UIDLExpressionValue | undefined,
68
93
  sortDirection: UIDLStaticValue | UIDLExpressionValue | undefined
@@ -17,14 +17,25 @@ const ADJACENT_POST_CANDIDATES = 5
17
17
  *
18
18
  * MUST mirror `findAdjacentBlogPosts` in teleport-gui
19
19
  * `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
20
+ *
21
+ * In Manual Order (`arranged`) a post walks the published posts of its
22
+ * PRIMARY category (the first of `category_ids`) the way the author arranged
23
+ * them: the ones with a place (`sort_order`) by it, then the rest newest
24
+ * first, ties by id — "previous" the one above it, "next" the one below. The
25
+ * category is read off each row in code, since a list cell may be JSON or a
26
+ * comma-separated list (see `array-overlap-sql.ts`). A post without a
27
+ * category, or a table the query cannot read (a column it lacks), walks the
28
+ * newest-first order above. MUST mirror `findArrangedAdjacentPosts` in
29
+ * teleport-gui `apps/gui/app/project-page/features/blog/utils/arranged-order.ts`.
20
30
  */
21
- export const generateAdjacentPostsCode = (): string => `
31
+ export const generateAdjacentPostsCode = (postsTable: string, arranged = false): string => `
22
32
  var BLOG_ADJACENT_CANDIDATES = ${ADJACENT_POST_CANDIDATES}
33
+ var BLOG_ARRANGED_ORDER = ${arranged}
23
34
 
24
35
  function blogAdjacentPostsSql(comparison, direction) {
25
36
  return (
26
- 'SELECT p.* FROM teleport_blog_posts p,' +
27
- ' (SELECT created_at, id FROM teleport_blog_posts WHERE id = $1) cur' +
37
+ 'SELECT p.* FROM ${postsTable} p,' +
38
+ ' (SELECT created_at, id FROM ${postsTable} WHERE id = $1) cur' +
28
39
  " WHERE p.status = 'published' AND (p.created_at, p.id) " + comparison + ' (cur.created_at, cur.id)' +
29
40
  ' ORDER BY p.created_at ' + direction + ', p.id ' + direction +
30
41
  ' LIMIT ' + BLOG_ADJACENT_CANDIDATES
@@ -44,6 +55,69 @@ function firstLinkableBlogPost(result) {
44
55
  return null
45
56
  }
46
57
 
58
+ function blogPlaceOf(row) {
59
+ var raw = pickFirst(row.sort_order, row.sortOrder)
60
+ if (raw === null || raw === undefined || raw === '') return null
61
+ var value = Number(raw)
62
+ return isFinite(value) ? value : null
63
+ }
64
+
65
+ // The arranged posts by their place, then the rest newest first, ties by id descending.
66
+ function compareBlogPlaces(a, b) {
67
+ var placeA = blogPlaceOf(a)
68
+ var placeB = blogPlaceOf(b)
69
+ if (placeA !== placeB) {
70
+ if (placeA === null) return 1
71
+ if (placeB === null) return -1
72
+ return placeA - placeB
73
+ }
74
+ var byDate =
75
+ normalizeTimestamp(pickFirst(b.created_at, b.createdAt)) -
76
+ normalizeTimestamp(pickFirst(a.created_at, a.createdAt))
77
+ if (byDate !== 0) return byDate
78
+ var idA = String(a.id)
79
+ var idB = String(b.id)
80
+ return idA < idB ? 1 : idA > idB ? -1 : 0
81
+ }
82
+
83
+ // { previous, next } along the post's primary category in Manual Order, or
84
+ // null when the post has none (the caller then walks the newest-first order).
85
+ async function getArrangedAdjacentPosts(client, record) {
86
+ var primary = parseJsonArray(record.category_ids)[0]
87
+ if (!primary) return null
88
+ var postId = String(record.id)
89
+ var listed = await client.query(
90
+ "SELECT id, category_ids, sort_order, created_at FROM ${postsTable} WHERE status = 'published' OR id = $1",
91
+ [postId]
92
+ )
93
+ var chain = (listed && Array.isArray(listed.rows) ? listed.rows : [])
94
+ .filter(function (row) {
95
+ return String(row.id) === postId || parseJsonArray(row.category_ids)[0] === primary
96
+ })
97
+ .sort(compareBlogPlaces)
98
+ var at = -1
99
+ for (var i = 0; i < chain.length; i++) {
100
+ if (String(chain[i].id) === postId) at = i
101
+ }
102
+ if (at < 0) return null
103
+ var above = chain.slice(Math.max(0, at - BLOG_ADJACENT_CANDIDATES), at).reverse()
104
+ var below = chain.slice(at + 1, at + 1 + BLOG_ADJACENT_CANDIDATES)
105
+ var ids = above.concat(below).map(function (row) { return String(row.id) })
106
+ if (ids.length === 0) return { previous: null, next: null }
107
+ var full = await client.query(
108
+ 'SELECT p.* FROM ${postsTable} p WHERE p.id::text = ANY($1::text[])',
109
+ [ids]
110
+ )
111
+ var byId = {}
112
+ ;(full && Array.isArray(full.rows) ? full.rows : []).forEach(function (row) {
113
+ byId[String(row.id)] = row
114
+ })
115
+ var rowsOf = function (entries) {
116
+ return { rows: entries.map(function (row) { return byId[String(row.id)] }) }
117
+ }
118
+ return { previous: firstLinkableBlogPost(rowsOf(above)), next: firstLinkableBlogPost(rowsOf(below)) }
119
+ }
120
+
47
121
  // { [postId]: { previous, next } } — raw rows or null — for a single-record
48
122
  // fetch; {} otherwise and on any failure (the navigation then hides).
49
123
  async function getAdjacentPostsMap(getClientFn, records) {
@@ -56,6 +130,18 @@ async function getAdjacentPostsMap(getClientFn, records) {
56
130
  try {
57
131
  client = getClientFn()
58
132
  await client.connect()
133
+ var arranged = null
134
+ if (BLOG_ARRANGED_ORDER) {
135
+ try {
136
+ arranged = await getArrangedAdjacentPosts(client, records[0])
137
+ } catch (e) {
138
+ // A table without the columns: the newest-first order below.
139
+ }
140
+ }
141
+ if (arranged) {
142
+ map[String(postId)] = arranged
143
+ return map
144
+ }
59
145
  var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])
60
146
  var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])
61
147
  map[String(postId)] = {
@@ -27,17 +27,17 @@ export const BLOG_COMMENTS_RENDER_LIMIT = 200
27
27
  * MUST mirror `buildBlogComments` in teleport-gui
28
28
  * `packages/renderer/src/utils/blog-comments.ts` (the canvas).
29
29
  */
30
- export const generateBlogCommentsCode = (): string => `
30
+ export const generateBlogCommentsCode = (commentsTable: string): string => `
31
31
  var BLOG_COMMENTS_RENDER_LIMIT = ${BLOG_COMMENTS_RENDER_LIMIT}
32
32
 
33
33
  var BLOG_COMMENTS_SQL =
34
34
  'WITH top AS (' +
35
- ' SELECT id FROM teleport_blog_comments' +
35
+ ' SELECT id FROM ${commentsTable}' +
36
36
  " WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'" +
37
37
  ' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +
38
38
  ')' +
39
39
  ' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +
40
- ' FROM teleport_blog_comments c' +
40
+ ' FROM ${commentsTable} c' +
41
41
  " WHERE c.post_id = $1 AND c.status = 'approved'" +
42
42
  ' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +
43
43
  ' ORDER BY c.created_at ASC, c.id ASC'
@@ -45,10 +45,10 @@ var BLOG_COMMENTS_SQL =
45
45
  // Every approved comment the page could show — a reply counts only while the
46
46
  // comment it answers is approved too.
47
47
  var BLOG_COMMENTS_COUNT_SQL =
48
- 'SELECT COUNT(*) AS n FROM teleport_blog_comments c' +
48
+ 'SELECT COUNT(*) AS n FROM ${commentsTable} c' +
49
49
  " WHERE c.post_id = $1 AND c.status = 'approved'" +
50
50
  ' AND (c.parent_id IS NULL OR EXISTS (' +
51
- ' SELECT 1 FROM teleport_blog_comments p' +
51
+ ' SELECT 1 FROM ${commentsTable} p' +
52
52
  " WHERE p.id = c.parent_id AND p.status = 'approved'))"
53
53
 
54
54
  function blogAuthorInitials(name) {