@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
@@ -1,4 +1,5 @@
1
1
  import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
2
+ import { ContentTables, RichTextContentCodegen } from '@teleporthq/teleport-shared'
2
3
  import { generateCategoryTaxonomyCode } from './category-taxonomy'
3
4
  import { generateAdjacentPostsCode } from './blog-adjacent-posts'
4
5
  import { generateBlogCommentsCode } from './blog-comments'
@@ -6,10 +7,23 @@ import { generateHeadingAnchorsCode } from './heading-anchors'
6
7
 
7
8
  /** What the blog-post transform bakes in at export time beyond the row itself. */
8
9
  export interface BlogPostTransformOptions {
10
+ /**
11
+ * The preset's tables (the Blog's by default): the posts table the related
12
+ * and adjacent posts are read from, the comments table a post page counts.
13
+ */
14
+ tables?: ContentTables.ContentTables
9
15
  /** Category taxonomy — lives only in the UIDL, there is no DB table for it. */
10
16
  categories?: UIDLEcommerceCategory[]
11
17
  /** Content headings get ids and `#` links — see `heading-anchors.ts`. */
12
18
  headingAnchors?: boolean
19
+ /**
20
+ * The post pages carry a Contents list: the content's headings get their ids
21
+ * (the `#` links only with `headingAnchors`) and a post page's post its
22
+ * `sections` — see `blogContentsSectionsOf`.
23
+ */
24
+ contents?: boolean
25
+ /** Manual Order: Previous / Next walk the post's category as arranged — see `blog-adjacent-posts.ts`. */
26
+ arranged?: boolean
13
27
  }
14
28
 
15
29
  /**
@@ -20,15 +34,28 @@ export interface BlogPostTransformOptions {
20
34
  export const generateBlogPostTransformationCode = (
21
35
  options: BlogPostTransformOptions = {}
22
36
  ): string => {
37
+ const tables = options.tables ?? ContentTables.contentTablesByKey('blog')
23
38
  return `
24
39
  // ============================================================
25
40
  // Blog Post Transformation
26
41
  // ============================================================
27
42
  ${generateCategoryTaxonomyCode('BLOG_CATEGORIES_BY_ID', options.categories)}
28
- ${generateBlogCommentsCode()}
29
- ${generateAdjacentPostsCode()}
43
+ ${generateBlogCommentsCode(tables.comments ?? `${tables.posts}_comments`)}
44
+ ${generateAdjacentPostsCode(tables.posts, options.arranged === true)}
30
45
  ${generateHeadingAnchorsCode()}
46
+ ${RichTextContentCodegen.generateLegacyRichTextNormalizerCode()}
31
47
  var BLOG_HEADING_ANCHORS = ${options.headingAnchors === true}
48
+ var BLOG_CONTENTS = ${options.contents === true}
49
+ var BLOG_PICTURE_STAND_IN = ${tables.pictureStandIn !== false}
50
+
51
+ // A post's reading time from its body, for one that stores none: its words at
52
+ // about 200 a minute, at least one — what the editor's post form computes. A
53
+ // card or post page otherwise printed "min read" with no number before it.
54
+ // MUST mirror readingTimeOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.
55
+ function blogReadingTimeOf(html) {
56
+ var text = String(html || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()
57
+ return text ? Math.max(1, Math.ceil(text.split(' ').length / 200)) : null
58
+ }
32
59
 
33
60
  function buildBlogPost(record, options) {
34
61
  if (!record || typeof record !== 'object') return record
@@ -42,8 +69,21 @@ function buildBlogPost(record, options) {
42
69
  // i18n-resolved text fields
43
70
  var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''
44
71
  var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''
45
- var content = resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
46
- if (BLOG_HEADING_ANCHORS) content = addHeadingAnchors(content)
72
+ // A body stored before the rich-text contract (the studio's flat list lines,
73
+ // the admin panel's no-break spaces) reads as stored HTML from here on.
74
+ var content = normalizeLegacyRichTextHtml(
75
+ resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
76
+ )
77
+ // Counted before the heading anchors add their marks.
78
+ var bodyReadingTime = blogReadingTimeOf(content)
79
+ // The Contents list links to the headings, so they get their ids for it too
80
+ // — the # links only with the heading anchors on. Its sections ride on a
81
+ // post page's own fetch only (options.details): a listing draws no list, and
82
+ // every card of it would otherwise carry one.
83
+ var contentsHeadings = BLOG_CONTENTS && options.details === true ? [] : null
84
+ if (BLOG_HEADING_ANCHORS || BLOG_CONTENTS) {
85
+ content = addHeadingAnchors(content, { links: BLOG_HEADING_ANCHORS, sections: contentsHeadings })
86
+ }
47
87
  var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''
48
88
  var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null
49
89
 
@@ -77,11 +117,15 @@ function buildBlogPost(record, options) {
77
117
  rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||
78
118
  featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG
79
119
  ) {
80
- featuredImageUrl =
81
- 'data:image/svg+xml;charset=utf-8,' +
82
- encodeURIComponent(
83
- '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240" viewBox="0 0 400 240"><rect fill="#e5e7eb" width="400" height="240"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#9ca3af" font-family="system-ui,sans-serif" font-size="13">No image</text></svg>'
84
- )
120
+ // No picture of its own: the grey "No image" stand-in, or none at all for a
121
+ // preset whose posts are text (ContentTables.pictureStandIn) — its card and
122
+ // post page show the picture only when there is one.
123
+ featuredImageUrl = BLOG_PICTURE_STAND_IN
124
+ ? 'data:image/svg+xml;charset=utf-8,' +
125
+ encodeURIComponent(
126
+ '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240" viewBox="0 0 400 240"><rect fill="#e5e7eb" width="400" height="240"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#9ca3af" font-family="system-ui,sans-serif" font-size="13">No image</text></svg>'
127
+ )
128
+ : null
85
129
  }
86
130
 
87
131
  var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)
@@ -90,8 +134,11 @@ function buildBlogPost(record, options) {
90
134
  // Simple pass-through fields
91
135
  var authorName = pickFirst(record.author_name, record.authorName)
92
136
  var authorInitials = blogAuthorInitials(authorName)
93
- var authorEmail = pickFirst(record.author_email, record.authorEmail)
137
+ // The author's email never leaves the server: a visitor's API read hides it
138
+ // (browser-row-policy.ts), and the page data a post or listing page ships
139
+ // is built here, so it is not part of the post a page sees either.
94
140
  var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)
141
+ if (!(readingTimeMinutes > 0)) readingTimeMinutes = bodyReadingTime
95
142
 
96
143
  // Tags - i18n-resolved then parsed as JSON array
97
144
  var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)
@@ -207,15 +254,16 @@ function buildBlogPost(record, options) {
207
254
  galleryImages: galleryImages,
208
255
  allImages: allImages,
209
256
  authorName: authorName,
210
- authorEmail: authorEmail,
211
257
  author_name: authorName,
212
- author_email: authorEmail,
213
258
  authorAvatarUrl: authorAvatarUrl,
214
259
  authorBio: authorBio,
215
260
  authorInitials: authorInitials,
216
261
  metaTitle: metaTitle,
217
262
  metaDescription: metaDescription,
218
263
  readingTimeMinutes: readingTimeMinutes,
264
+ // The post's sections, what its page's Contents list links to — see
265
+ // blogContentsSectionsOf; empty off a post page or under three sections.
266
+ sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],
219
267
  isFeatured: isFeatured,
220
268
  allowComments: allowComments,
221
269
  comments: comments,
@@ -231,6 +279,9 @@ function buildBlogPost(record, options) {
231
279
  createdAt: createdAt,
232
280
  updatedAt: updatedAt,
233
281
  created: created,
282
+ // The post's place among its primary category's posts (Manual Order), or
283
+ // null: what the category pages order by.
284
+ sortOrder: safeNumber(pickFirst(record.sort_order, record.sortOrder), null),
234
285
  }
235
286
  }
236
287
 
@@ -261,7 +312,7 @@ async function getRelatedPostsMap(getClientFn, records) {
261
312
  try {
262
313
  client = getClientFn()
263
314
  await client.connect()
264
- var result = await client.query('SELECT * FROM teleport_blog_posts WHERE id = ANY($1)', [
315
+ var result = await client.query('SELECT * FROM ${tables.posts} WHERE id = ANY($1)', [
265
316
  wantedIds,
266
317
  ])
267
318
  if (result && result.rows) {
@@ -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
+ }
@@ -9,22 +9,51 @@
9
9
  export const BLOG_HEADING_ANCHOR_CLASS = 'tq-heading-anchor'
10
10
 
11
11
  /**
12
- * Generates `addHeadingAnchors(html)`: every `<h2>`–`<h6>` of a post's content
13
- * gets an `id` (kept when it already has one, otherwise a slug of its text,
14
- * unique within the post) and a trailing `#` link to itself, so a section can
15
- * be linked to (`/blog/post#setup`) and the link works on the very first load —
16
- * the ids are in the server-rendered HTML.
12
+ * The class of the link a post's content heading's own text becomes.
17
13
  *
18
- * The link repeats the heading a screen reader already announces, so it is
19
- * hidden from assistive technology and left out of the tab order; the heading
20
- * stays the landmark.
14
+ * ⛔ PAIRED with `BLOG_HEADING_LINK_CLASS` in teleport-gui
15
+ * `features/blog/utils/elements/heading-anchor-styles.ts`, whose stylesheet
16
+ * keeps the heading looking like a heading rather than like a link.
17
+ */
18
+ export const BLOG_HEADING_LINK_CLASS = 'tq-heading-link'
19
+
20
+ /**
21
+ * The fewest sections a post's Contents list is drawn for: a shorter post is
22
+ * taken in at a glance.
23
+ *
24
+ * ⛔ PAIRED with `CONTENTS_MIN_SECTIONS` in teleport-gui
25
+ * `packages/renderer/src/utils/blog-posts.ts`, the canvas's copy.
26
+ */
27
+ export const CONTENTS_MIN_SECTIONS = 3
28
+
29
+ /**
30
+ * Generates `addHeadingAnchors(html, options)`: every `<h2>`–`<h6>` of a post's
31
+ * content gets an `id` (kept when it already has one, otherwise a slug of its
32
+ * text, unique within the post), its text becomes a link to itself and a `#`
33
+ * follows it, shown on hover, so a section can be linked to
34
+ * (`/blog/post#setup`) by clicking its heading, and the link works on the very
35
+ * first load — the ids are in the server-rendered HTML. A heading that holds a
36
+ * link of its own keeps its text as it is (a link cannot hold another) and
37
+ * links through the `#` alone.
38
+ *
39
+ * `options.links: false` gives the headings their ids without the links —
40
+ * what a post page's Contents list needs when the heading anchors are off.
41
+ * `options.sections`, an array, collects every heading as `{ level, id, text }`
42
+ * in reading order, and `blogContentsSectionsOf` turns those into the list.
43
+ *
44
+ * The heading's text is the link a screen reader announces and the keyboard
45
+ * reaches; the `#` repeats it, so it is hidden from assistive technology and
46
+ * left out of the tab order.
21
47
  *
22
48
  * Idempotent: a heading that already carries the link is left alone. `h1` is
23
49
  * the post title's level and is never touched.
24
50
  */
25
51
  export const generateHeadingAnchorsCode = (): string => `
26
52
  var BLOG_HEADING_ANCHOR_CLASS = '${BLOG_HEADING_ANCHOR_CLASS}'
53
+ var BLOG_HEADING_LINK_CLASS = '${BLOG_HEADING_LINK_CLASS}'
27
54
  var BLOG_HEADING_ID_PATTERN = /\\sid\\s*=\\s*("([^"]*)"|'([^']*)')/i
55
+ var BLOG_HEADING_ANCHOR_LINK_PATTERN = /<a\\s[^>]*class="${BLOG_HEADING_ANCHOR_CLASS}"[^>]*>[\\s\\S]*?<\\/a>/gi
56
+ var BLOG_CONTENTS_MIN_SECTIONS = ${CONTENTS_MIN_SECTIONS}
28
57
 
29
58
  function escapeHeadingAttribute(value) {
30
59
  return String(value)
@@ -52,11 +81,15 @@ function blogHeadingSlug(text, used) {
52
81
  return slug
53
82
  }
54
83
 
55
- function addHeadingAnchors(html) {
84
+ function addHeadingAnchors(html, options) {
85
+ options = options || {}
86
+ var withLinks = options.links !== false
87
+ var sections = Array.isArray(options.sections) ? options.sections : null
56
88
  if (typeof html !== 'string' || !/<h[2-6][\\s>]/i.test(html)) return html
57
89
  // Every id the content already uses is taken, so a generated one never
58
- // duplicates it.
59
- var used = {}
90
+ // duplicates it. No prototype: a heading titled "Constructor" is free to be
91
+ // #constructor.
92
+ var used = Object.create(null)
60
93
  var existingIds = new RegExp(BLOG_HEADING_ID_PATTERN.source, 'gi')
61
94
  var existing
62
95
  while ((existing = existingIds.exec(html)) !== null) {
@@ -64,22 +97,52 @@ function addHeadingAnchors(html) {
64
97
  }
65
98
 
66
99
  return html.replace(/<h([2-6])(\\s[^>]*)?>([\\s\\S]*?)<\\/h\\1>/gi, function(match, level, rawAttrs, inner) {
67
- if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) return match
68
- var text = stripHtmlTags(inner)
69
- if (!text) return match
70
100
  var attrs = rawAttrs || ''
71
101
  var idMatch = attrs.match(BLOG_HEADING_ID_PATTERN)
72
102
  var id = idMatch ? decodeHtmlEntities(idMatch[2] || idMatch[3] || '') : ''
103
+ if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) {
104
+ if (sections && id) {
105
+ var anchoredText = stripHtmlTags(inner.replace(BLOG_HEADING_ANCHOR_LINK_PATTERN, ''))
106
+ if (anchoredText) sections.push({ level: Number(level), id: id, text: anchoredText })
107
+ }
108
+ return match
109
+ }
110
+ var text = stripHtmlTags(inner)
111
+ if (!text) return match
73
112
  if (!id) {
74
113
  id = blogHeadingSlug(text, used)
75
114
  attrs = ' id="' + escapeHeadingAttribute(id) + '"' + attrs
76
115
  }
116
+ if (sections) sections.push({ level: Number(level), id: id, text: text })
117
+ if (!withLinks) return '<h' + level + attrs + '>' + inner + '</h' + level + '>'
118
+ var href = '#' + escapeHeadingAttribute(id)
119
+ var ownText = /<a[\\s>]/i.test(inner)
120
+ ? inner
121
+ : '<a class="' + BLOG_HEADING_LINK_CLASS + '" href="' + href + '">' + inner + '</a>'
77
122
  return (
78
- '<h' + level + attrs + '>' + inner +
79
- '<a class="' + BLOG_HEADING_ANCHOR_CLASS + '" href="#' + escapeHeadingAttribute(id) +
123
+ '<h' + level + attrs + '>' + ownText +
124
+ '<a class="' + BLOG_HEADING_ANCHOR_CLASS + '" href="' + href +
80
125
  '" aria-hidden="true" tabindex="-1">#</a>' +
81
126
  '</h' + level + '>'
82
127
  )
83
128
  })
84
129
  }
130
+
131
+ // A post page's Contents list: the post's sections, i.e. its headings of the
132
+ // topmost level it uses, each with the address that jumps to it; none for a
133
+ // post of fewer than BLOG_CONTENTS_MIN_SECTIONS sections.
134
+ // MUST mirror contentsSectionsOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.
135
+ function blogContentsSectionsOf(headings) {
136
+ if (!Array.isArray(headings) || headings.length === 0) return []
137
+ var top = 6
138
+ for (var i = 0; i < headings.length; i++) {
139
+ if (headings[i].level < top) top = headings[i].level
140
+ }
141
+ var sections = []
142
+ for (var j = 0; j < headings.length; j++) {
143
+ if (headings[j].level !== top) continue
144
+ sections.push({ id: headings[j].id, title: headings[j].text, href: '#' + headings[j].id })
145
+ }
146
+ return sections.length >= BLOG_CONTENTS_MIN_SECTIONS ? sections : []
147
+ }
85
148
  `
@@ -1,12 +1,14 @@
1
1
  import type {
2
2
  GeneratorOptions,
3
+ UIDLBlogSettings,
3
4
  UIDLAuthentication,
4
5
  UIDLEcommerceCategory,
5
6
  } from '@teleporthq/teleport-types'
6
- import { StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
7
+ import { ContentTables, StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
7
8
  import { generateSharedTransformationCode } from './shared-utils'
8
9
  import { generateBlogPostTransformationCode } from './blog-post'
9
10
  import { generateCustomPageTransformationCode } from './custom-page'
11
+ import { generateBookingItemTransformationCode } from './booking-item'
10
12
  import {
11
13
  generateEcommerceProductTransformationCode,
12
14
  type EcommerceProductTransformOptions,
@@ -26,9 +28,26 @@ export type { EcommerceProductTransformOptions }
26
28
  * to serve whichever transform that resolves to — the product fields are ignored
27
29
  * for a blog table and vice versa.
28
30
  */
31
+ /** What a content preset bakes into its post transform — see `UIDLBlogSettings`. */
32
+ export interface ContentPresetTransformSettings {
33
+ categories?: UIDLEcommerceCategory[]
34
+ headingAnchors?: boolean
35
+ contents?: boolean
36
+ comments?: boolean
37
+ order?: 'manual'
38
+ }
39
+
29
40
  export interface EntityTransformOptions extends EcommerceProductTransformOptions {
41
+ /**
42
+ * The settings of every content preset, by preset key — the post transform
43
+ * of a table reads its own preset's (`contentSettingsFor`). The `blog*`
44
+ * fields below are the Blog's, kept for the callers that name them.
45
+ */
46
+ contentSettings?: Partial<Record<ContentTables.ContentPresetKey, ContentPresetTransformSettings>>
30
47
  /** Blog post-category taxonomy — see `blogSettings.categories`. */
31
48
  blogCategories?: UIDLEcommerceCategory[]
49
+ /** The booking items' category taxonomy — see `bookingSettings.categories`. */
50
+ bookingCategories?: UIDLEcommerceCategory[]
32
51
  /** Post content headings get ids and `#` links — see `blogSettings.headingAnchors`. */
33
52
  blogHeadingAnchors?: boolean
34
53
  /**
@@ -77,10 +96,55 @@ const resolveAllowBackorders = (
77
96
  * tax-inclusive or untaxed stores. `allowBackorders` is the effective
78
97
  * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
79
98
  */
99
+ const contentPresetTransformSettings = (
100
+ settings: UIDLBlogSettings | undefined
101
+ ): ContentPresetTransformSettings => ({
102
+ categories: settings?.categories,
103
+ headingAnchors: settings?.headingAnchors === true,
104
+ ...(settings?.contents === true && { contents: true }),
105
+ comments: settings?.comments === true,
106
+ ...(settings?.order === 'manual' && { order: 'manual' as const }),
107
+ })
108
+
109
+ /**
110
+ * The settings the transform of `tableName` bakes in: its preset's entry in
111
+ * `contentSettings`, or — for the Blog, and for a caller that only set the
112
+ * `blog*` fields — those fields. Empty for a table no preset owns.
113
+ */
114
+ export const contentSettingsFor = (
115
+ options: Pick<
116
+ EntityTransformOptions,
117
+ 'contentSettings' | 'blogCategories' | 'blogHeadingAnchors' | 'blogComments'
118
+ >,
119
+ tableName: string
120
+ ): ContentPresetTransformSettings => {
121
+ const role = ContentTables.contentTableRole(stripSchemaQualifier(tableName))
122
+ if (!role) {
123
+ return {}
124
+ }
125
+ const own = options.contentSettings?.[role.key]
126
+ if (own) {
127
+ return own
128
+ }
129
+ if (role.key === 'blog') {
130
+ return {
131
+ categories: options.blogCategories,
132
+ headingAnchors: options.blogHeadingAnchors,
133
+ comments: options.blogComments,
134
+ }
135
+ }
136
+ return {}
137
+ }
138
+
80
139
  export const buildProductTransformOptions = (
81
140
  options: Pick<
82
141
  GeneratorOptions,
83
- 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'auth'
142
+ | 'ecommerceSettings'
143
+ | 'invoiceSettings'
144
+ | 'blogSettings'
145
+ | 'bookingSettings'
146
+ | 'helpCenterSettings'
147
+ | 'auth'
84
148
  > &
85
149
  LocalizedProjectOptions & {
86
150
  /**
@@ -96,7 +160,12 @@ export const buildProductTransformOptions = (
96
160
  }
97
161
  ): EntityTransformOptions => ({
98
162
  categories: options.ecommerceSettings?.categories,
163
+ contentSettings: {
164
+ blog: contentPresetTransformSettings(options.blogSettings),
165
+ help: contentPresetTransformSettings(options.helpCenterSettings),
166
+ },
99
167
  blogCategories: options.blogSettings?.categories,
168
+ bookingCategories: options.bookingSettings?.categories,
100
169
  blogHeadingAnchors: options.blogSettings?.headingAnchors === true,
101
170
  blogComments: options.blogSettings?.comments === true,
102
171
  storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
@@ -106,7 +175,12 @@ export const buildProductTransformOptions = (
106
175
  trustedReaderRoles: TableAccess.resolveTrustedReaderRoles(options.auth || options.authentication),
107
176
  })
108
177
 
109
- 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
110
184
 
111
185
  /**
112
186
  * Strips a leading `schema.` qualifier from a table name so that only the
@@ -125,9 +199,9 @@ const stripSchemaQualifier = (tableName: string): string => {
125
199
  * Detects which transformation type to apply based on the table name.
126
200
  * Returns null if no transformation is needed.
127
201
  *
128
- * IMPORTANT: Only the real platform-managed tables (`teleport_products`,
129
- * `teleport_blog_posts`) are routed through the e-commerce/blog view-model
130
- * transforms. A previous loose substring match (`lower.includes('products')`)
202
+ * IMPORTANT: Only the real platform-managed tables (`teleport_products`, the
203
+ * content presets' posts tables) are routed through the e-commerce/blog
204
+ * view-model transforms. A previous loose substring match (`lower.includes('products')`)
131
205
  * incorrectly routed CUSTOM tables such as `products`, `store_products` or
132
206
  * `wholesale_products` through `buildEcommerceProduct`, which emits a fixed set
133
207
  * of platform product fields and silently drops all custom columns. Matching the
@@ -138,7 +212,7 @@ export const detectTransformationType = (tableName: string): TransformationType
138
212
  return null
139
213
  }
140
214
  const bare = stripSchemaQualifier(tableName)
141
- if (bare === 'teleport_blog_posts') {
215
+ if (ContentTables.isContentPostsTable(bare)) {
142
216
  return 'blog-post'
143
217
  }
144
218
  if (bare === 'teleport_products') {
@@ -147,6 +221,9 @@ export const detectTransformationType = (tableName: string): TransformationType
147
221
  if (bare === 'teleport_pages') {
148
222
  return 'custom-page'
149
223
  }
224
+ if (bare === 'teleport_booking_items') {
225
+ return 'booking-item'
226
+ }
150
227
  return null
151
228
  }
152
229
 
@@ -167,18 +244,28 @@ export const getTransformationCode = (
167
244
  const shared = generateSharedTransformationCode()
168
245
 
169
246
  switch (type) {
170
- case 'blog-post':
247
+ case 'blog-post': {
248
+ const role = ContentTables.contentTableRole(stripSchemaQualifier(tableName))
249
+ const settings = contentSettingsFor(options, tableName)
171
250
  return (
172
251
  shared +
173
252
  generateBlogPostTransformationCode({
174
- categories: options.blogCategories,
175
- headingAnchors: options.blogHeadingAnchors,
253
+ tables: role ? ContentTables.contentTablesByKey(role.key) : undefined,
254
+ categories: settings.categories,
255
+ headingAnchors: settings.headingAnchors,
256
+ contents: settings.contents === true,
257
+ arranged: settings.order === 'manual',
176
258
  })
177
259
  )
260
+ }
178
261
  case 'ecommerce-product':
179
262
  return shared + generateEcommerceProductTransformationCode(options)
180
263
  case 'custom-page':
181
264
  return shared + generateCustomPageTransformationCode()
265
+ case 'booking-item':
266
+ return (
267
+ shared + generateBookingItemTransformationCode({ categories: options.bookingCategories })
268
+ )
182
269
  default:
183
270
  return ''
184
271
  }
@@ -300,7 +387,7 @@ export const REVIEWS_PER_PRODUCT = 5
300
387
  */
301
388
  export const getTransformWrapperCode = (
302
389
  tableName: string,
303
- options: Pick<EntityTransformOptions, 'localization' | 'blogComments'> = {}
390
+ options: Pick<EntityTransformOptions, 'localization' | 'blogComments' | 'contentSettings'> = {}
304
391
  ): string => {
305
392
  const type = detectTransformationType(tableName)
306
393
  if (!type) {
@@ -314,6 +401,8 @@ export const getTransformWrapperCode = (
314
401
  ? 'transformBlogPosts'
315
402
  : type === 'custom-page'
316
403
  ? 'transformCustomPages'
404
+ : type === 'booking-item'
405
+ ? 'transformBookingItems'
317
406
  : 'transformEcommerceProducts'
318
407
 
319
408
  // Products additionally get their purchasable variant combinations attached in
@@ -422,7 +511,8 @@ export const getTransformWrapperCode = (
422
511
 
423
512
  // A post page's neighbours and comments, on the same single-record heuristic
424
513
  // as the related rail. The comments are only asked for when the blog takes
425
- // them — see `EntityTransformOptions.blogComments`.
514
+ // them — see `EntityTransformOptions.blogComments`. `details` tells the
515
+ // transform the same thing: what only a post page draws (its Contents list).
426
516
  const blogPageEnrichment =
427
517
  type === 'blog-post'
428
518
  ? `
@@ -434,7 +524,7 @@ export const getTransformWrapperCode = (
434
524
  } catch (e) {
435
525
  // Best-effort; the post navigation stays hidden.
436
526
  }${
437
- options.blogComments
527
+ contentSettingsFor(options, tableName).comments === true
438
528
  ? `
439
529
  try {
440
530
  commentsByPostId = await getBlogCommentsMap(getClientFn, records)
@@ -447,7 +537,7 @@ export const getTransformWrapperCode = (
447
537
  : ''
448
538
  const blogPageOption =
449
539
  type === 'blog-post'
450
- ? ', adjacentPostsById: adjacentPostsById, commentsByPostId: commentsByPostId'
540
+ ? ', adjacentPostsById: adjacentPostsById, commentsByPostId: commentsByPostId, details: Array.isArray(records) && records.length === 1'
451
541
  : ''
452
542
 
453
543
  return `${RAW_ROWS_CODE}