@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.
- package/__tests__/_helpers/pagination-widget-fixture.ts +95 -5
- package/__tests__/blog-adjacent-arranged.test.ts +176 -0
- package/__tests__/blog-post-contents.test.ts +212 -0
- package/__tests__/blog-post-page-enrichment.test.ts +28 -5
- package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
- package/__tests__/blog-post-reading-time.test.ts +55 -0
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +13 -3
- package/__tests__/content-preset-wiring.test.ts +108 -0
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/fixed-list-sort.test.ts +183 -0
- package/__tests__/infinite-scroll.test.ts +154 -5
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/pagination-filter-groups.test.ts +186 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +215 -0
- package/__tests__/product-price-sort.ts +58 -25
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/rest-api-key-header.test.ts +18 -0
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +61 -16
- package/__tests__/shared-resource-prefetch.test.ts +65 -0
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- package/dist/cjs/count-effect.d.ts +7 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +15 -5
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
- package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/cjs/fetchers/pg-client-import.js +15 -0
- package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +4 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +2 -1
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +5 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts +19 -7
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +49 -13
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +25 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +144 -33
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/list-sort-order.d.ts +14 -0
- package/dist/cjs/list-sort-order.d.ts.map +1 -0
- package/dist/cjs/list-sort-order.js +36 -0
- package/dist/cjs/list-sort-order.js.map +1 -0
- package/dist/cjs/pagination-controls.d.ts +22 -3
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +29 -3
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +267 -212
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +0 -9
- package/dist/cjs/product-price-sort.d.ts.map +1 -1
- package/dist/cjs/product-price-sort.js +1 -13
- package/dist/cjs/product-price-sort.js.map +1 -1
- package/dist/cjs/range-control.d.ts +45 -0
- package/dist/cjs/range-control.d.ts.map +1 -0
- package/dist/cjs/range-control.js +165 -0
- package/dist/cjs/range-control.js.map +1 -0
- package/dist/cjs/row-order.d.ts +20 -0
- package/dist/cjs/row-order.d.ts.map +1 -0
- package/dist/cjs/row-order.js +46 -0
- package/dist/cjs/row-order.js.map +1 -0
- package/dist/cjs/search-input.d.ts +42 -0
- package/dist/cjs/search-input.d.ts.map +1 -0
- package/dist/cjs/search-input.js +158 -0
- package/dist/cjs/search-input.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +10 -0
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +22 -1
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-comments.js +1 -1
- package/dist/cjs/transformations/blog-comments.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +14 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/booking-item.d.ts +24 -0
- package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
- package/dist/cjs/transformations/booking-item.js +27 -0
- package/dist/cjs/transformations/booking-item.js.map +1 -0
- package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/cjs/transformations/heading-anchors.js +34 -10
- package/dist/cjs/transformations/heading-anchors.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +27 -13
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +73 -16
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +52 -3
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/count-effect.d.ts +7 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +15 -5
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
- package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/esm/fetchers/pg-client-import.js +12 -0
- package/dist/esm/fetchers/pg-client-import.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +5 -2
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +2 -1
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +6 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/filter-utils.d.ts +19 -7
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +46 -12
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +25 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +142 -32
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/list-sort-order.d.ts +14 -0
- package/dist/esm/list-sort-order.d.ts.map +1 -0
- package/dist/esm/list-sort-order.js +32 -0
- package/dist/esm/list-sort-order.js.map +1 -0
- package/dist/esm/pagination-controls.d.ts +22 -3
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +28 -2
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +271 -216
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +0 -9
- package/dist/esm/product-price-sort.d.ts.map +1 -1
- package/dist/esm/product-price-sort.js +0 -11
- package/dist/esm/product-price-sort.js.map +1 -1
- package/dist/esm/range-control.d.ts +45 -0
- package/dist/esm/range-control.d.ts.map +1 -0
- package/dist/esm/range-control.js +137 -0
- package/dist/esm/range-control.js.map +1 -0
- package/dist/esm/row-order.d.ts +20 -0
- package/dist/esm/row-order.d.ts.map +1 -0
- package/dist/esm/row-order.js +41 -0
- package/dist/esm/row-order.js.map +1 -0
- package/dist/esm/search-input.d.ts +42 -0
- package/dist/esm/search-input.d.ts.map +1 -0
- package/dist/esm/search-input.js +129 -0
- package/dist/esm/search-input.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +10 -0
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +20 -0
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/esm/transformations/blog-comments.d.ts +1 -1
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
- package/dist/esm/transformations/blog-comments.js +1 -1
- package/dist/esm/transformations/blog-comments.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +14 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/booking-item.d.ts +24 -0
- package/dist/esm/transformations/booking-item.d.ts.map +1 -0
- package/dist/esm/transformations/booking-item.js +23 -0
- package/dist/esm/transformations/booking-item.js.map +1 -0
- package/dist/esm/transformations/heading-anchors.d.ts +32 -8
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/esm/transformations/heading-anchors.js +33 -9
- package/dist/esm/transformations/heading-anchors.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +27 -13
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +72 -16
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +52 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/count-effect.ts +31 -8
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +25 -23
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/rest-api.ts +3 -1
- package/src/fetchers/teleport.ts +22 -23
- package/src/fetchers/utils/browser-row-policy.ts +10 -20
- package/src/filter-utils.ts +102 -28
- package/src/infinite-scroll.ts +240 -79
- package/src/list-sort-order.ts +34 -0
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +370 -306
- package/src/product-price-sort.ts +0 -11
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +55 -0
- package/src/search-input.ts +190 -0
- package/src/sort-utils.ts +25 -0
- package/src/transformations/blog-adjacent-posts.ts +89 -3
- package/src/transformations/blog-comments.ts +5 -5
- package/src/transformations/blog-post.ts +64 -13
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/heading-anchors.ts +79 -16
- package/src/transformations/index.ts +104 -14
- package/src/utils.ts +74 -3
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as types from '@babel/types'
|
|
2
2
|
import { ChunkType, FileType, ChunkDefinition } from '@teleporthq/teleport-types'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
PAGINATION_CONTROL_ATTR,
|
|
5
|
+
PAGINATION_FOR_ATTR,
|
|
6
|
+
RANGE_TEMPLATE_ATTR,
|
|
7
|
+
} from '../../src/pagination-controls'
|
|
8
|
+
import { SEARCH_FOR_ATTR } from '../../src/search-input'
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* A component shell carrying a `DataProvider` and the pagination controls a
|
|
@@ -20,9 +25,36 @@ interface ControlOptions {
|
|
|
20
25
|
legacyOnly?: boolean
|
|
21
26
|
/** Number of DataProvider + widget pairs to emit (order-based zip coverage). */
|
|
22
27
|
mappers?: number
|
|
28
|
+
/** The element the providers render in: a table's body, a list. */
|
|
29
|
+
rootTag?: string
|
|
30
|
+
/**
|
|
31
|
+
* The `data-tq-range-template` of a `range` control in the widget. Defaults
|
|
32
|
+
* to the "Showing" wording; `null` authors the control with no template.
|
|
33
|
+
*/
|
|
34
|
+
rangeTemplate?: string | null
|
|
35
|
+
/**
|
|
36
|
+
* A `range` text OUTSIDE every widget, placed BEFORE the first provider (a
|
|
37
|
+
* toolbar above the list), naming its mapper with `data-tq-pagination-for`.
|
|
38
|
+
*/
|
|
39
|
+
standaloneRange?: { forNodeId: string; template?: string }
|
|
40
|
+
/**
|
|
41
|
+
* Search inputs OUTSIDE every widget, placed BEFORE the first provider, in
|
|
42
|
+
* this order: a page's own box naming its mapper with `data-tq-search-for`,
|
|
43
|
+
* or the platform's (`search-input` class) paired by order.
|
|
44
|
+
*/
|
|
45
|
+
standaloneSearch?: Array<{ forNodeId?: string; name: string }>
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
// ASCII hyphen on purpose: the generator prints string literals through jsesc,
|
|
49
|
+
// which would turn an en dash into `–` and make every assertion unreadable.
|
|
50
|
+
export const RANGE_DRAWN_TEXT = 'Showing 1-8 of 12'
|
|
51
|
+
export const RANGE_TEMPLATE = 'Showing {from}–{to} of {total}'
|
|
52
|
+
|
|
53
|
+
const control = (
|
|
54
|
+
marker: string,
|
|
55
|
+
children: types.JSXElement['children'] = [],
|
|
56
|
+
extraAttrs: Array<[string, string]> = []
|
|
57
|
+
): types.JSXElement =>
|
|
26
58
|
types.jsxElement(
|
|
27
59
|
types.jsxOpeningElement(
|
|
28
60
|
types.jsxIdentifier('div'),
|
|
@@ -35,6 +67,9 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
|
|
|
35
67
|
types.jsxIdentifier(PAGINATION_CONTROL_ATTR),
|
|
36
68
|
types.stringLiteral(marker)
|
|
37
69
|
),
|
|
70
|
+
...extraAttrs.map(([name, value]) =>
|
|
71
|
+
types.jsxAttribute(types.jsxIdentifier(name), types.stringLiteral(value))
|
|
72
|
+
),
|
|
38
73
|
],
|
|
39
74
|
false
|
|
40
75
|
),
|
|
@@ -43,6 +78,40 @@ const control = (marker: string, children: types.JSXElement['children'] = []): t
|
|
|
43
78
|
false
|
|
44
79
|
)
|
|
45
80
|
|
|
81
|
+
const rangeControl = (
|
|
82
|
+
template: string | null | undefined,
|
|
83
|
+
forNodeId?: string
|
|
84
|
+
): types.JSXElement => {
|
|
85
|
+
const attrs: Array<[string, string]> = []
|
|
86
|
+
if (template !== null) {
|
|
87
|
+
attrs.push([RANGE_TEMPLATE_ATTR, template ?? RANGE_TEMPLATE])
|
|
88
|
+
}
|
|
89
|
+
if (forNodeId) {
|
|
90
|
+
attrs.push([PAGINATION_FOR_ATTR, forNodeId])
|
|
91
|
+
}
|
|
92
|
+
return control('range', [types.jsxText(RANGE_DRAWN_TEXT)], attrs)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const searchInput = (name: string, forNodeId?: string): types.JSXElement =>
|
|
96
|
+
types.jsxElement(
|
|
97
|
+
types.jsxOpeningElement(
|
|
98
|
+
types.jsxIdentifier('input'),
|
|
99
|
+
[
|
|
100
|
+
types.jsxAttribute(types.jsxIdentifier('name'), types.stringLiteral(name)),
|
|
101
|
+
forNodeId
|
|
102
|
+
? types.jsxAttribute(types.jsxIdentifier(SEARCH_FOR_ATTR), types.stringLiteral(forNodeId))
|
|
103
|
+
: types.jsxAttribute(
|
|
104
|
+
types.jsxIdentifier('className'),
|
|
105
|
+
types.stringLiteral('products-list-search-input')
|
|
106
|
+
),
|
|
107
|
+
],
|
|
108
|
+
true
|
|
109
|
+
),
|
|
110
|
+
null,
|
|
111
|
+
[],
|
|
112
|
+
true
|
|
113
|
+
)
|
|
114
|
+
|
|
46
115
|
const legacyControl = (direction: 'previous' | 'next'): types.JSXElement =>
|
|
47
116
|
types.jsxElement(
|
|
48
117
|
types.jsxOpeningElement(
|
|
@@ -72,6 +141,10 @@ const buildWidget = (options: ControlOptions): types.JSXElement => {
|
|
|
72
141
|
children.push(legacyControl(marker))
|
|
73
142
|
continue
|
|
74
143
|
}
|
|
144
|
+
if (marker === 'range') {
|
|
145
|
+
children.push(rangeControl(options.rangeTemplate))
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
75
148
|
children.push(control(marker))
|
|
76
149
|
}
|
|
77
150
|
|
|
@@ -148,6 +221,12 @@ const buildDataProvider = (name: string, rowIdentifier: string): types.JSXElemen
|
|
|
148
221
|
|
|
149
222
|
export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefinition => {
|
|
150
223
|
const sections: types.JSXElement[] = []
|
|
224
|
+
if (options.standaloneRange) {
|
|
225
|
+
sections.push(rangeControl(options.standaloneRange.template, options.standaloneRange.forNodeId))
|
|
226
|
+
}
|
|
227
|
+
for (const search of options.standaloneSearch ?? []) {
|
|
228
|
+
sections.push(searchInput(search.name, search.forNodeId))
|
|
229
|
+
}
|
|
151
230
|
const mappers = options.mappers ?? 1
|
|
152
231
|
for (let i = 0; i < mappers; i += 1) {
|
|
153
232
|
const identifier = `items${i === 0 ? '' : i}`
|
|
@@ -155,9 +234,10 @@ export const makeWidgetComponentChunk = (options: ControlOptions): ChunkDefiniti
|
|
|
155
234
|
sections.push(buildWidget(options))
|
|
156
235
|
}
|
|
157
236
|
|
|
237
|
+
const rootTag = options.rootTag ?? 'div'
|
|
158
238
|
const root = types.jsxElement(
|
|
159
|
-
types.jsxOpeningElement(types.jsxIdentifier(
|
|
160
|
-
types.jsxClosingElement(types.jsxIdentifier(
|
|
239
|
+
types.jsxOpeningElement(types.jsxIdentifier(rootTag), [], false),
|
|
240
|
+
types.jsxClosingElement(types.jsxIdentifier(rootTag)),
|
|
161
241
|
sections,
|
|
162
242
|
false
|
|
163
243
|
)
|
|
@@ -187,6 +267,10 @@ export interface RepeaterOptions {
|
|
|
187
267
|
infiniteScroll?: boolean
|
|
188
268
|
infiniteScrollLoadMore?: boolean
|
|
189
269
|
perPage?: number
|
|
270
|
+
/** The list's filters, as the inspector authors them (conditions and groups). */
|
|
271
|
+
filters?: unknown[]
|
|
272
|
+
/** The mapper's editor node id, as the UIDL carries it on the repeater. */
|
|
273
|
+
nodeId?: string
|
|
190
274
|
}
|
|
191
275
|
|
|
192
276
|
export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'items'): any => ({
|
|
@@ -198,7 +282,12 @@ export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'ite
|
|
|
198
282
|
tableName: 'products',
|
|
199
283
|
dataSourceType: 'postgresql',
|
|
200
284
|
},
|
|
201
|
-
resource: {
|
|
285
|
+
resource: {
|
|
286
|
+
params: {
|
|
287
|
+
queryColumns: { content: ['name'] },
|
|
288
|
+
...(options.filters ? { filters: { type: 'static', content: options.filters } } : {}),
|
|
289
|
+
},
|
|
290
|
+
},
|
|
202
291
|
nodes: {
|
|
203
292
|
success: {
|
|
204
293
|
type: 'cms-list-repeater',
|
|
@@ -211,6 +300,7 @@ export const makeUidlNode = (options: RepeaterOptions, identifier: string = 'ite
|
|
|
211
300
|
...(options.paginationMode ? { paginationMode: options.paginationMode } : {}),
|
|
212
301
|
...(options.infiniteScroll ? { infiniteScroll: true } : {}),
|
|
213
302
|
...(options.infiniteScrollLoadMore ? { infiniteScrollLoadMore: true } : {}),
|
|
303
|
+
...(options.nodeId ? { nodeId: options.nodeId } : {}),
|
|
214
304
|
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
215
305
|
},
|
|
216
306
|
},
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { generateBlogPostTransformationCode } from '../src/transformations/blog-post'
|
|
2
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
3
|
+
import { getTransformationCode } from '../src/transformations'
|
|
4
|
+
import { ContentTables } from '@teleporthq/teleport-shared'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Previous / Next in Manual Order: a post walks the published posts of its
|
|
8
|
+
* primary category the way the author arranged them. These evaluate the
|
|
9
|
+
* EMITTED code against a fake database client, the way the runtime calls it.
|
|
10
|
+
*
|
|
11
|
+
* The fixture and the expectations are the SAME as teleport-gui
|
|
12
|
+
* `features/blog/utils/__tests__/arranged-order.spec.ts` ("Previous / Next the
|
|
13
|
+
* published site mirrors"): the canvas and the site must agree.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface Row {
|
|
17
|
+
id: string
|
|
18
|
+
status: string
|
|
19
|
+
category_ids: string | null
|
|
20
|
+
sort_order: number | null
|
|
21
|
+
created_at: string
|
|
22
|
+
redirect_url?: string | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type AdjacentMap = Record<string, { previous: Row | null; next: Row | null }>
|
|
26
|
+
type GetAdjacent = (getClient: () => unknown, records: unknown[]) => Promise<AdjacentMap>
|
|
27
|
+
|
|
28
|
+
const HELP_TABLE = ContentTables.contentTablesByKey('help').posts
|
|
29
|
+
|
|
30
|
+
const load = (arranged: boolean): GetAdjacent =>
|
|
31
|
+
// tslint:disable-next-line:function-constructor
|
|
32
|
+
new Function(
|
|
33
|
+
`${generateSharedTransformationCode()}\n${generateBlogPostTransformationCode({
|
|
34
|
+
tables: ContentTables.contentTablesByKey('help'),
|
|
35
|
+
arranged,
|
|
36
|
+
})}\nreturn getAdjacentPostsMap;`
|
|
37
|
+
)() as GetAdjacent
|
|
38
|
+
|
|
39
|
+
const row = (
|
|
40
|
+
id: string,
|
|
41
|
+
categories: string[] | null,
|
|
42
|
+
sortOrder: number | null,
|
|
43
|
+
createdAt: string,
|
|
44
|
+
overrides: Partial<Row> = {}
|
|
45
|
+
): Row => ({
|
|
46
|
+
id,
|
|
47
|
+
status: 'published',
|
|
48
|
+
category_ids: categories ? JSON.stringify(categories) : null,
|
|
49
|
+
sort_order: sortOrder,
|
|
50
|
+
created_at: createdAt,
|
|
51
|
+
...overrides,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const TABLE: Row[] = [
|
|
55
|
+
row('a1', ['guides'], 1, '2026-01-01T00:00:00.000Z'),
|
|
56
|
+
row('a2', ['guides'], 2, '2026-01-05T00:00:00.000Z'),
|
|
57
|
+
row('a3', ['guides'], 3, '2026-01-03T00:00:00.000Z', { redirect_url: '/help/a1' }),
|
|
58
|
+
row('a4', ['guides'], 4, '2026-01-02T00:00:00.000Z'),
|
|
59
|
+
row('a5', ['guides'], null, '2026-02-01T00:00:00.000Z'),
|
|
60
|
+
row('a6', ['guides'], null, '2026-01-20T00:00:00.000Z'),
|
|
61
|
+
row('draft', ['guides'], 2, '2026-03-01T00:00:00.000Z', { status: 'draft' }),
|
|
62
|
+
// Filed under guides as a second category: not on guides' path.
|
|
63
|
+
row('b1', ['billing', 'guides'], 1, '2026-01-04T00:00:00.000Z'),
|
|
64
|
+
// A comma-separated cell, as the generated admin panel's text field writes it.
|
|
65
|
+
row('c1', null, 1, '2026-01-06T00:00:00.000Z', { category_ids: 'guides, billing' }),
|
|
66
|
+
row('lone', null, null, '2026-01-07T00:00:00.000Z'),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
/** A client over `TABLE` that answers the statements the code sends, and records them. */
|
|
70
|
+
const fakeClient = (options: { failArranged?: boolean } = {}) => {
|
|
71
|
+
const statements: string[] = []
|
|
72
|
+
const client = {
|
|
73
|
+
connect: async (): Promise<void> => undefined,
|
|
74
|
+
end: async (): Promise<void> => undefined,
|
|
75
|
+
query: async (sql: string, params: unknown[]) => {
|
|
76
|
+
statements.push(sql)
|
|
77
|
+
if (sql.startsWith('SELECT id, category_ids, sort_order, created_at FROM')) {
|
|
78
|
+
if (options.failArranged) {
|
|
79
|
+
throw new Error('column "sort_order" does not exist')
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
rows: TABLE.filter((entry) => entry.status === 'published' || entry.id === params[0]),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (sql.includes('= ANY($1::text[])')) {
|
|
86
|
+
const ids = params[0] as string[]
|
|
87
|
+
return { rows: TABLE.filter((entry) => ids.includes(entry.id)) }
|
|
88
|
+
}
|
|
89
|
+
// The newest-first lookups: (created_at, id) before / after the post's.
|
|
90
|
+
const current = TABLE.find((entry) => entry.id === params[0]) as Row
|
|
91
|
+
const older = sql.includes(') < (cur.created_at')
|
|
92
|
+
const rows = TABLE.filter((entry) => entry.status === 'published')
|
|
93
|
+
.filter((entry) =>
|
|
94
|
+
older ? entry.created_at < current.created_at : entry.created_at > current.created_at
|
|
95
|
+
)
|
|
96
|
+
.sort((a, b) =>
|
|
97
|
+
older
|
|
98
|
+
? b.created_at.localeCompare(a.created_at)
|
|
99
|
+
: a.created_at.localeCompare(b.created_at)
|
|
100
|
+
)
|
|
101
|
+
return { rows }
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
return { client, statements }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const neighbours = async (postId: string, arranged = true, failArranged = false) => {
|
|
108
|
+
const { client, statements } = fakeClient({ failArranged })
|
|
109
|
+
const map = await load(arranged)(
|
|
110
|
+
() => client,
|
|
111
|
+
[TABLE.find((candidate) => candidate.id === postId)]
|
|
112
|
+
)
|
|
113
|
+
const found = map[postId]
|
|
114
|
+
return {
|
|
115
|
+
previous: found?.previous?.id ?? null,
|
|
116
|
+
next: found?.next?.id ?? null,
|
|
117
|
+
statements,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('Previous / Next in Manual Order', () => {
|
|
122
|
+
it('walks the primary category as arranged: places first, then the rest newest first', async () => {
|
|
123
|
+
// c1 shares place 1 and is newer, so it comes first.
|
|
124
|
+
expect(await neighbours('a1')).toMatchObject({ previous: 'c1', next: 'a2' })
|
|
125
|
+
// a3 redirects elsewhere: stepped over both ways.
|
|
126
|
+
expect(await neighbours('a2')).toMatchObject({ previous: 'a1', next: 'a4' })
|
|
127
|
+
expect(await neighbours('a4')).toMatchObject({ previous: 'a2', next: 'a5' })
|
|
128
|
+
expect(await neighbours('a5')).toMatchObject({ previous: 'a4', next: 'a6' })
|
|
129
|
+
expect(await neighbours('a6')).toMatchObject({ previous: 'a5', next: null })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('leaves out drafts and posts filed there through a second category, yet places a draft it shows', async () => {
|
|
133
|
+
const draft = await neighbours('draft')
|
|
134
|
+
expect(draft).toMatchObject({ previous: 'a1', next: 'a2' })
|
|
135
|
+
expect(await neighbours('b1')).toMatchObject({ previous: null, next: null })
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('reads a comma-separated category cell like a JSON one', async () => {
|
|
139
|
+
// c1's primary is guides; place 1 ties with a1, the newer first.
|
|
140
|
+
expect(await neighbours('c1')).toMatchObject({ previous: null, next: 'a1' })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('walks the newest-first order for a post without a category, or a table it cannot read', async () => {
|
|
144
|
+
const lone = await neighbours('lone')
|
|
145
|
+
expect(lone.statements.some((sql) => sql.includes('(cur.created_at, cur.id)'))).toBe(true)
|
|
146
|
+
expect(lone).toMatchObject({ previous: 'c1', next: 'a6' })
|
|
147
|
+
|
|
148
|
+
const unreadable = await neighbours('a2', true, true)
|
|
149
|
+
expect(unreadable).toMatchObject({ previous: 'b1', next: 'c1' })
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('never asks for the arranged order when the pages are newest first', async () => {
|
|
153
|
+
const plain = await neighbours('a2', false)
|
|
154
|
+
expect(plain.statements.some((sql) => sql.includes('sort_order'))).toBe(false)
|
|
155
|
+
expect(plain).toMatchObject({ previous: 'b1', next: 'c1' })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('is emitted for a table whose preset is in Manual Order, and only then', () => {
|
|
159
|
+
const arranged = getTransformationCode(HELP_TABLE, {
|
|
160
|
+
contentSettings: { help: { order: 'manual' } },
|
|
161
|
+
})
|
|
162
|
+
expect(arranged).toContain('var BLOG_ARRANGED_ORDER = true')
|
|
163
|
+
expect(getTransformationCode(HELP_TABLE, {})).toContain('var BLOG_ARRANGED_ORDER = false')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('the post a page is given', () => {
|
|
168
|
+
it('carries its place, or null', () => {
|
|
169
|
+
const build = new Function(
|
|
170
|
+
`${generateSharedTransformationCode()}\n${generateBlogPostTransformationCode()}\nreturn buildBlogPost;`
|
|
171
|
+
)() as (record: unknown) => Record<string, unknown>
|
|
172
|
+
expect(build(row('a', ['guides'], 3, '2026-01-01T00:00:00.000Z')).sortOrder).toBe(3)
|
|
173
|
+
expect(build(row('a', ['guides'], null, '2026-01-01T00:00:00.000Z')).sortOrder).toBeNull()
|
|
174
|
+
expect(build({ id: 'old', title: 'Old', slug: 'old' }).sortOrder).toBeNull()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { generateBlogPostTransformationCode } from '../src/transformations/blog-post'
|
|
2
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
3
|
+
import {
|
|
4
|
+
buildProductTransformOptions,
|
|
5
|
+
getTransformationCode,
|
|
6
|
+
getTransformWrapperCode,
|
|
7
|
+
} from '../src/transformations'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A post page's Contents list: the post's sections — its headings of the
|
|
11
|
+
* topmost level it uses — each with the address that jumps to it, drawn for a
|
|
12
|
+
* post of three sections or more. The headings get their ids for it even with
|
|
13
|
+
* the heading anchors off, and only a post page's own fetch carries the list.
|
|
14
|
+
*
|
|
15
|
+
* `CONTENTS_FIXTURES` is the SAME table as teleport-gui `packages/renderer/src/
|
|
16
|
+
* utils/__tests__/blog-contents-sections.test.ts`: the canvas and the site must
|
|
17
|
+
* list the same sections under the same addresses.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type Build = (record: unknown, options?: Record<string, unknown>) => Record<string, unknown>
|
|
21
|
+
|
|
22
|
+
const buildWith = (options: { headingAnchors?: boolean; contents?: boolean } = {}): Build =>
|
|
23
|
+
new Function(
|
|
24
|
+
generateSharedTransformationCode() +
|
|
25
|
+
'\n' +
|
|
26
|
+
generateBlogPostTransformationCode(options) +
|
|
27
|
+
'\nreturn buildBlogPost;'
|
|
28
|
+
)() as Build
|
|
29
|
+
|
|
30
|
+
const POST = { id: 'p1', title: 'Connect a domain', slug: 'connect-a-domain', status: 'published' }
|
|
31
|
+
const DETAILS = { details: true }
|
|
32
|
+
|
|
33
|
+
const CONTENTS_FIXTURES: Array<{ name: string; html: string; sections: string[][] }> = [
|
|
34
|
+
{
|
|
35
|
+
name: 'a help article: its h2 sections, not the h3 under them, never the h1',
|
|
36
|
+
html:
|
|
37
|
+
'<h1>Title</h1><p>Intro</p><h2>Before you start</h2><p>x</p><h2>Steps</h2>' +
|
|
38
|
+
'<h3>Open the panel</h3><h2>Check that it worked</h2><h2>Troubleshooting</h2>',
|
|
39
|
+
sections: [
|
|
40
|
+
['before-you-start', 'Before you start'],
|
|
41
|
+
['steps', 'Steps'],
|
|
42
|
+
['check-that-it-worked', 'Check that it worked'],
|
|
43
|
+
['troubleshooting', 'Troubleshooting'],
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'fewer than three sections: no list',
|
|
48
|
+
html: '<h2>One</h2><p>x</p><h2>Two</h2><h3>Two and a half</h3><h3>Two and three quarters</h3>',
|
|
49
|
+
sections: [],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'the topmost level the post uses',
|
|
53
|
+
html: '<h3>Alpha</h3><h4>Under alpha</h4><h3>Beta</h3><h3>Gamma</h3>',
|
|
54
|
+
sections: [
|
|
55
|
+
['alpha', 'Alpha'],
|
|
56
|
+
['beta', 'Beta'],
|
|
57
|
+
['gamma', 'Gamma'],
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'ids the content has are kept, repeated titles numbered, entities decoded',
|
|
62
|
+
html:
|
|
63
|
+
'<h2 id="custom">Café & Crème</h2><h2>Setup</h2><h2>Setup</h2>' +
|
|
64
|
+
'<h2 id="setup-2">Clash</h2>',
|
|
65
|
+
sections: [
|
|
66
|
+
['custom', 'Café & Crème'],
|
|
67
|
+
['setup', 'Setup'],
|
|
68
|
+
['setup-3', 'Setup'],
|
|
69
|
+
['setup-2', 'Clash'],
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'an empty heading is no section',
|
|
74
|
+
html: '<h2><br></h2><h2>A</h2><h2>B</h2><h2>C</h2>',
|
|
75
|
+
sections: [
|
|
76
|
+
['a', 'A'],
|
|
77
|
+
['b', 'B'],
|
|
78
|
+
['c', 'C'],
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'accented, non-Latin and symbol-only titles',
|
|
83
|
+
html: '<h2>Crème brûlée</h2><h2>Привет мир</h2><h2>!!!</h2>',
|
|
84
|
+
sections: [
|
|
85
|
+
['creme-brulee', 'Crème brûlée'],
|
|
86
|
+
['привет-мир', 'Привет мир'],
|
|
87
|
+
['section', '!!!'],
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'a title that names a built-in property',
|
|
92
|
+
html: '<h2>Constructor</h2><h2>Prototype</h2><h2>Has own property</h2>',
|
|
93
|
+
sections: [
|
|
94
|
+
['constructor', 'Constructor'],
|
|
95
|
+
['prototype', 'Prototype'],
|
|
96
|
+
['has-own-property', 'Has own property'],
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'inline markup inside a heading',
|
|
101
|
+
html: '<h2>Use <code>npm</code> <3</h2><h2><strong>Bold</strong> move</h2><h2>Last</h2>',
|
|
102
|
+
sections: [
|
|
103
|
+
['use-npm-3', 'Use npm <3'],
|
|
104
|
+
['bold-move', 'Bold move'],
|
|
105
|
+
['last', 'Last'],
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
const asPairs = (sections: unknown) =>
|
|
111
|
+
(sections as Array<{ id: string; title: string; href: string }>).map((section) => {
|
|
112
|
+
expect(section.href).toBe(`#${section.id}`)
|
|
113
|
+
return [section.id, section.title]
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('a post page’s Contents list', () => {
|
|
117
|
+
it.each(CONTENTS_FIXTURES)('$name', ({ html, sections }) => {
|
|
118
|
+
const built = buildWith({ contents: true })({ ...POST, content: html }, DETAILS)
|
|
119
|
+
expect(asPairs(built.sections)).toEqual(sections)
|
|
120
|
+
// Every section the list links to is a heading of the page, under that id.
|
|
121
|
+
for (const [id] of sections) {
|
|
122
|
+
expect(built.content as string).toContain(`id="${id}"`)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('gives the headings their ids without the # links while the heading anchors are off', () => {
|
|
127
|
+
const built = buildWith({ contents: true })(
|
|
128
|
+
{ ...POST, content: '<h2>One</h2><h2>Two</h2><h2>Three</h2>' },
|
|
129
|
+
DETAILS
|
|
130
|
+
)
|
|
131
|
+
expect(built.content).toBe(
|
|
132
|
+
'<h2 id="one">One</h2><h2 id="two">Two</h2><h2 id="three">Three</h2>'
|
|
133
|
+
)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('links the same ids the heading anchors give, with the anchors on', () => {
|
|
137
|
+
const built = buildWith({ contents: true, headingAnchors: true })(
|
|
138
|
+
{ ...POST, content: '<h2>One</h2><h2>Two</h2><h2>Three</h2>' },
|
|
139
|
+
DETAILS
|
|
140
|
+
)
|
|
141
|
+
expect(built.content).toContain(
|
|
142
|
+
'<h2 id="one"><a class="tq-heading-link" href="#one">One</a><a class="tq-heading-anchor" href="#one" aria-hidden="true" tabindex="-1">#</a></h2>'
|
|
143
|
+
)
|
|
144
|
+
expect(asPairs(built.sections)).toEqual([
|
|
145
|
+
['one', 'One'],
|
|
146
|
+
['two', 'Two'],
|
|
147
|
+
['three', 'Three'],
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('reads a heading stored as its own link with the # after it, without the #', () => {
|
|
152
|
+
const stored =
|
|
153
|
+
'<h2 id="one"><a class="tq-heading-link" href="#one">One</a><a class="tq-heading-anchor" href="#one" aria-hidden="true" tabindex="-1">#</a></h2>' +
|
|
154
|
+
'<h2>Two</h2><h2>Three</h2>'
|
|
155
|
+
const built = buildWith({ contents: true, headingAnchors: true })(
|
|
156
|
+
{ ...POST, content: stored },
|
|
157
|
+
DETAILS
|
|
158
|
+
)
|
|
159
|
+
expect(asPairs(built.sections)).toEqual([
|
|
160
|
+
['one', 'One'],
|
|
161
|
+
['two', 'Two'],
|
|
162
|
+
['three', 'Three'],
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('reads a heading stored with its # link already, without the #', () => {
|
|
167
|
+
const stored =
|
|
168
|
+
'<h2 id="one">One<a class="tq-heading-anchor" href="#one" aria-hidden="true" tabindex="-1">#</a></h2>' +
|
|
169
|
+
'<h2>Two</h2><h2>Three</h2>'
|
|
170
|
+
const built = buildWith({ contents: true, headingAnchors: true })(
|
|
171
|
+
{ ...POST, content: stored },
|
|
172
|
+
DETAILS
|
|
173
|
+
)
|
|
174
|
+
expect(asPairs(built.sections)).toEqual([
|
|
175
|
+
['one', 'One'],
|
|
176
|
+
['two', 'Two'],
|
|
177
|
+
['three', 'Three'],
|
|
178
|
+
])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('is left off a listing’s cards and off a post without the list', () => {
|
|
182
|
+
const content = '<h2>One</h2><h2>Two</h2><h2>Three</h2>'
|
|
183
|
+
const card = buildWith({ contents: true })({ ...POST, content })
|
|
184
|
+
expect(card.sections).toEqual([])
|
|
185
|
+
expect(card.content).toContain('id="one"')
|
|
186
|
+
|
|
187
|
+
const withoutList = buildWith()({ ...POST, content }, DETAILS)
|
|
188
|
+
expect(withoutList.sections).toEqual([])
|
|
189
|
+
expect(withoutList.content).toBe(content)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('the Contents setting, from the project to the fetcher', () => {
|
|
194
|
+
const HELP_TABLE = 'teleport_help_articles'
|
|
195
|
+
|
|
196
|
+
it('reaches the transform of its own preset only', () => {
|
|
197
|
+
const options = buildProductTransformOptions({
|
|
198
|
+
helpCenterSettings: { headingAnchors: false, contents: true },
|
|
199
|
+
blogSettings: { headingAnchors: true },
|
|
200
|
+
})
|
|
201
|
+
expect(getTransformationCode(HELP_TABLE, options)).toContain('var BLOG_CONTENTS = true')
|
|
202
|
+
expect(getTransformationCode('teleport_blog_posts', options)).toContain(
|
|
203
|
+
'var BLOG_CONTENTS = false'
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('tells the transform when the fetch is a post page’s own', () => {
|
|
208
|
+
expect(getTransformWrapperCode(HELP_TABLE)).toContain(
|
|
209
|
+
'details: Array.isArray(records) && records.length === 1'
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
@@ -29,6 +29,16 @@ const post = (id: string, overrides: Record<string, unknown> = {}) => ({
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
describe('blog post transform — author box fields', () => {
|
|
32
|
+
it('keeps the author email out of the post a page is given, under both spellings', () => {
|
|
33
|
+
const built = buildBlogPost(
|
|
34
|
+
post('a', { author_name: 'Ana', author_email: 'ana@example.com' })
|
|
35
|
+
) as Record<string, unknown>
|
|
36
|
+
expect(built.authorName).toBe('Ana')
|
|
37
|
+
expect('authorEmail' in built).toBe(false)
|
|
38
|
+
expect('author_email' in built).toBe(false)
|
|
39
|
+
expect(JSON.stringify(built)).not.toContain('ana@example.com')
|
|
40
|
+
})
|
|
41
|
+
|
|
32
42
|
it('resolves the bio in the request language, falling back to the main one', () => {
|
|
33
43
|
const record = post('a', {
|
|
34
44
|
author_bio: 'Writes about tea.',
|
|
@@ -152,7 +162,7 @@ describe('blog post transform — heading anchors', () => {
|
|
|
152
162
|
expect(buildBlogPost(post('a', { content })).content).toBe(content)
|
|
153
163
|
})
|
|
154
164
|
|
|
155
|
-
it('gives every h2-h6 a unique id
|
|
165
|
+
it('gives every h2-h6 a unique id, makes its text a link to itself with a # after it, keeping ids it already has', () => {
|
|
156
166
|
const built = buildWith({ headingAnchors: true })(
|
|
157
167
|
post('a', {
|
|
158
168
|
content:
|
|
@@ -160,13 +170,26 @@ describe('blog post transform — heading anchors', () => {
|
|
|
160
170
|
'<h2>Getting started</h2><h4><br></h4><h2 id="getting-started-2">Clash</h2>',
|
|
161
171
|
})
|
|
162
172
|
)
|
|
173
|
+
const anchored = (tag: string, attrs: string, id: string, text: string) =>
|
|
174
|
+
`<${tag}${attrs}><a class="tq-heading-link" href="#${id}">${text}</a>` +
|
|
175
|
+
`<a class="tq-heading-anchor" href="#${id}" aria-hidden="true" tabindex="-1">#</a></${tag}>`
|
|
163
176
|
expect(built.content).toBe(
|
|
164
177
|
'<h1>Title</h1>' +
|
|
165
|
-
'
|
|
166
|
-
'
|
|
167
|
-
'
|
|
178
|
+
anchored('h2', ' id="getting-started"', 'getting-started', 'Getting Started') +
|
|
179
|
+
anchored('h3', ' class="x" id="custom"', 'custom', 'Café & Crème') +
|
|
180
|
+
anchored('h2', ' id="getting-started-3"', 'getting-started-3', 'Getting started') +
|
|
168
181
|
'<h4><br></h4>' +
|
|
169
|
-
'
|
|
182
|
+
anchored('h2', ' id="getting-started-2"', 'getting-started-2', 'Clash')
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('links a heading that holds a link of its own through the # alone', () => {
|
|
187
|
+
const built = buildWith({ headingAnchors: true })(
|
|
188
|
+
post('a', { content: '<h2>See <a href="/pricing">pricing</a></h2>' })
|
|
189
|
+
)
|
|
190
|
+
expect(built.content).toBe(
|
|
191
|
+
'<h2 id="see-pricing">See <a href="/pricing">pricing</a>' +
|
|
192
|
+
'<a class="tq-heading-anchor" href="#see-pricing" aria-hidden="true" tabindex="-1">#</a></h2>'
|
|
170
193
|
)
|
|
171
194
|
})
|
|
172
195
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ContentTables } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { generateBlogPostTransformationCode } from '../src/transformations/blog-post'
|
|
3
|
+
import { generateSharedTransformationCode } from '../src/transformations/shared-utils'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a post without a picture carries to its card and post page. The Blog
|
|
7
|
+
* shows the grey "No image" stand-in; the Help Center's articles are text, so
|
|
8
|
+
* an article without a picture carries none and its card closes up (the card
|
|
9
|
+
* draws the picture only when there is one). The editor reads the same
|
|
10
|
+
* `ContentTables.pictureStandIn`, so the canvas and the site agree.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const buildPost = (
|
|
14
|
+
key: ContentTables.ContentPresetKey
|
|
15
|
+
): ((record: unknown) => Record<string, unknown>) => {
|
|
16
|
+
const code =
|
|
17
|
+
generateSharedTransformationCode() +
|
|
18
|
+
'\n' +
|
|
19
|
+
generateBlogPostTransformationCode({ tables: ContentTables.contentTablesByKey(key) })
|
|
20
|
+
return new Function(code + '\nreturn buildBlogPost;')() as (
|
|
21
|
+
record: unknown
|
|
22
|
+
) => Record<string, unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const POST = { id: 'p1', title: 'Connect a domain', slug: 'connect-a-domain', status: 'published' }
|
|
26
|
+
const DEFAULT_PICTURE = 'https://play.teleporthq.io/static/svg/default-img.svg'
|
|
27
|
+
|
|
28
|
+
describe('a post without a picture', () => {
|
|
29
|
+
it('gets the grey stand-in on the Blog', () => {
|
|
30
|
+
expect(buildPost('blog')(POST).featuredImageUrl).toMatch(/^data:image\/svg\+xml/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('carries none in the Help Center, so its card closes up', () => {
|
|
34
|
+
expect(buildPost('help')(POST).featuredImageUrl).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('counts the platform default picture as none', () => {
|
|
38
|
+
const record = { ...POST, featured_image_url: DEFAULT_PICTURE }
|
|
39
|
+
expect(buildPost('help')(record).featuredImageUrl).toBeNull()
|
|
40
|
+
expect(buildPost('blog')(record).featuredImageUrl).toMatch(/^data:image\/svg\+xml/)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('keeps a picture of its own in both', () => {
|
|
44
|
+
const record = { ...POST, featured_image_url: 'https://cdn.example.com/domains.png' }
|
|
45
|
+
expect(buildPost('help')(record).featuredImageUrl).toBe('https://cdn.example.com/domains.png')
|
|
46
|
+
expect(buildPost('blog')(record).featuredImageUrl).toBe('https://cdn.example.com/domains.png')
|
|
47
|
+
})
|
|
48
|
+
})
|