@teleporthq/teleport-plugin-next-data-source 0.43.57 → 0.43.60
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 +230 -0
- package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
- package/__tests__/ecommerce-product-discounts.test.ts +196 -0
- package/__tests__/ecommerce-product-model.test.ts +179 -0
- package/__tests__/infinite-scroll.test.ts +143 -0
- package/__tests__/numbered-pagination.test.ts +113 -0
- package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
- package/__tests__/page-url-sync.test.ts +208 -0
- package/__tests__/product-price-sort.ts +313 -0
- package/__tests__/search-url-sync.test.ts +7 -3
- package/dist/cjs/array-mapper-pagination.d.ts +9 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
- package/dist/cjs/array-mapper-pagination.js.map +1 -1
- package/dist/cjs/count-effect.d.ts +42 -0
- package/dist/cjs/count-effect.d.ts.map +1 -0
- package/dist/cjs/count-effect.js +81 -0
- package/dist/cjs/count-effect.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +65 -0
- package/dist/cjs/infinite-scroll.d.ts.map +1 -0
- package/dist/cjs/infinite-scroll.js +190 -0
- package/dist/cjs/infinite-scroll.js.map +1 -0
- package/dist/cjs/numbered-pagination.d.ts +42 -0
- package/dist/cjs/numbered-pagination.d.ts.map +1 -0
- package/dist/cjs/numbered-pagination.js +167 -0
- package/dist/cjs/numbered-pagination.js.map +1 -0
- package/dist/cjs/page-reset.d.ts +42 -0
- package/dist/cjs/page-reset.d.ts.map +1 -0
- package/dist/cjs/page-reset.js +116 -0
- package/dist/cjs/page-reset.js.map +1 -0
- package/dist/cjs/page-url-sync.d.ts +42 -0
- package/dist/cjs/page-url-sync.d.ts.map +1 -0
- package/dist/cjs/page-url-sync.js +180 -0
- package/dist/cjs/page-url-sync.js.map +1 -0
- package/dist/cjs/pagination-controls.d.ts +55 -0
- package/dist/cjs/pagination-controls.d.ts.map +1 -0
- package/dist/cjs/pagination-controls.js +159 -0
- package/dist/cjs/pagination-controls.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +506 -160
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +116 -0
- package/dist/cjs/product-price-sort.d.ts.map +1 -0
- package/dist/cjs/product-price-sort.js +169 -0
- package/dist/cjs/product-price-sort.js.map +1 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts +12 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +2 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +2 -1
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +17 -1
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/array-mapper-pagination.d.ts +9 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
- package/dist/esm/array-mapper-pagination.js.map +1 -1
- package/dist/esm/count-effect.d.ts +42 -0
- package/dist/esm/count-effect.d.ts.map +1 -0
- package/dist/esm/count-effect.js +53 -0
- package/dist/esm/count-effect.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +2 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +3 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +65 -0
- package/dist/esm/infinite-scroll.d.ts.map +1 -0
- package/dist/esm/infinite-scroll.js +159 -0
- package/dist/esm/infinite-scroll.js.map +1 -0
- package/dist/esm/numbered-pagination.d.ts +42 -0
- package/dist/esm/numbered-pagination.d.ts.map +1 -0
- package/dist/esm/numbered-pagination.js +138 -0
- package/dist/esm/numbered-pagination.js.map +1 -0
- package/dist/esm/page-reset.d.ts +42 -0
- package/dist/esm/page-reset.d.ts.map +1 -0
- package/dist/esm/page-reset.js +87 -0
- package/dist/esm/page-reset.js.map +1 -0
- package/dist/esm/page-url-sync.d.ts +42 -0
- package/dist/esm/page-url-sync.d.ts.map +1 -0
- package/dist/esm/page-url-sync.js +152 -0
- package/dist/esm/page-url-sync.js.map +1 -0
- package/dist/esm/pagination-controls.d.ts +55 -0
- package/dist/esm/pagination-controls.d.ts.map +1 -0
- package/dist/esm/pagination-controls.js +126 -0
- package/dist/esm/pagination-controls.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +507 -161
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +116 -0
- package/dist/esm/product-price-sort.d.ts.map +1 -0
- package/dist/esm/product-price-sort.js +160 -0
- package/dist/esm/product-price-sort.js.map +1 -0
- package/dist/esm/transformations/ecommerce-product.d.ts +12 -0
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +3 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +2 -1
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +17 -1
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/array-mapper-pagination.ts +10 -0
- package/src/count-effect.ts +137 -0
- package/src/fetchers/postgresql.ts +52 -13
- package/src/fetchers/teleport.ts +53 -13
- package/src/infinite-scroll.ts +401 -0
- package/src/numbered-pagination.ts +292 -0
- package/src/page-reset.ts +162 -0
- package/src/page-url-sync.ts +241 -0
- package/src/pagination-controls.ts +176 -0
- package/src/pagination-plugin.ts +755 -346
- package/src/product-price-sort.ts +204 -0
- package/src/transformations/ecommerce-product.ts +141 -10
- package/src/transformations/index.ts +19 -1
- package/src/transformations/shared-utils.ts +14 -0
- package/dist/cjs/array-mapper-registry.d.ts +0 -85
- package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
- package/dist/cjs/array-mapper-registry.js +0 -296
- package/dist/cjs/array-mapper-registry.js.map +0 -1
- package/dist/cjs/pagination-with-count.d.ts +0 -6
- package/dist/cjs/pagination-with-count.d.ts.map +0 -1
- package/dist/cjs/pagination-with-count.js +0 -63
- package/dist/cjs/pagination-with-count.js.map +0 -1
- package/dist/esm/array-mapper-registry.d.ts +0 -85
- package/dist/esm/array-mapper-registry.d.ts.map +0 -1
- package/dist/esm/array-mapper-registry.js +0 -292
- package/dist/esm/array-mapper-registry.js.map +0 -1
- package/dist/esm/pagination-with-count.d.ts +0 -6
- package/dist/esm/pagination-with-count.d.ts.map +0 -1
- package/dist/esm/pagination-with-count.js +0 -34
- package/dist/esm/pagination-with-count.js.map +0 -1
- package/src/array-mapper-registry.ts +0 -416
- package/src/pagination-with-count.ts +0 -89
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering a products list by the price customers actually pay.
|
|
3
|
+
*
|
|
4
|
+
* A per-product discount is a SCHEDULED VIEW of the price: `teleport_products.
|
|
5
|
+
* price` holds the list price and never moves, while `teleport_products.
|
|
6
|
+
* discounts` holds a JSON array of markdowns with `[startsAt, endsAt)` windows.
|
|
7
|
+
* Every display surface resolves that at read time.
|
|
8
|
+
*
|
|
9
|
+
* Sorting is the one surface that cannot, because it happens in SQL before the
|
|
10
|
+
* rows ever reach JavaScript — and the list is paginated, so re-sorting the
|
|
11
|
+
* fetched page would only reorder 20 of N products. The ordering therefore has
|
|
12
|
+
* to be computed by the database, which is what this expression does: an inline
|
|
13
|
+
* scalar sub-select over the JSON column, evaluated per query against `now()`.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT a stored column. A materialised "effective price" is correct
|
|
16
|
+
* only until a schedule boundary passes with nobody writing that row, and
|
|
17
|
+
* keeping it fresh needs a job no generated store has.
|
|
18
|
+
*
|
|
19
|
+
* ## The expression cannot raise
|
|
20
|
+
*
|
|
21
|
+
* It runs inside `ORDER BY` for the whole products table, so one unparseable
|
|
22
|
+
* row would fail the entire query and empty the page. Every cast is therefore
|
|
23
|
+
* guarded:
|
|
24
|
+
*
|
|
25
|
+
* - the JSON cast only happens for text that already looks like an array;
|
|
26
|
+
* - `value` is cast only when it matches a bare decimal;
|
|
27
|
+
* - the window bounds are compared as TEXT, never cast to a timestamp. The
|
|
28
|
+
* editor writes them with `toISOString()`, whose fixed-width UTC format
|
|
29
|
+
* (`YYYY-MM-DDTHH:MM:SS.sssZ`) sorts lexicographically in the same order it
|
|
30
|
+
* sorts chronologically.
|
|
31
|
+
*
|
|
32
|
+
* The caller adds a second net: a query using this expression falls back to a
|
|
33
|
+
* plain column sort if the database rejects it, so an unforeseen shape degrades
|
|
34
|
+
* to the previous ordering rather than to a broken page.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** The e-commerce products table — the only table this ordering applies to. */
|
|
38
|
+
export const PRODUCTS_TABLE_NAME = 'teleport_products'
|
|
39
|
+
|
|
40
|
+
/** The sort field the storefront's "Price (low to high / high to low)" emits. */
|
|
41
|
+
export const PRODUCT_PRICE_SORT_FIELD = 'price'
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The sort field the storefront's "Discount (low to high / high to low)" emits.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately the name of a REAL column rather than a virtual one like
|
|
47
|
+
* `discount`. The same field travels to sixteen different fetchers and to the
|
|
48
|
+
* editor's own data API, none of which know about this rewrite — and a name
|
|
49
|
+
* that is not a column becomes `ORDER BY discount` and fails the whole query.
|
|
50
|
+
* Using the column the markdowns are stored in means the worst any unaware
|
|
51
|
+
* backend can do is order by the raw JSON: arbitrary, but never an error.
|
|
52
|
+
*/
|
|
53
|
+
export const PRODUCT_DISCOUNT_SORT_FIELD = 'discounts'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The column a rewritten sort falls back to when the expression cannot run —
|
|
57
|
+
* guaranteed to exist on the products table, which `discounts` itself is not on
|
|
58
|
+
* a store provisioned before discounts shipped.
|
|
59
|
+
*/
|
|
60
|
+
export const PRODUCT_SORT_FALLBACK_COLUMN = 'price'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `now()` as the same fixed-width UTC string the editor stores its bounds in,
|
|
64
|
+
* so the window comparison is pure text and can never raise on a bad date.
|
|
65
|
+
*/
|
|
66
|
+
const NOW_AS_ISO_TEXT = `to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Only text that already looks like a JSON array is cast; anything else — NULL,
|
|
70
|
+
* empty, or a hand-edited scalar — reads as "no discounts".
|
|
71
|
+
*/
|
|
72
|
+
const SAFE_DISCOUNTS_JSONB = `CASE
|
|
73
|
+
WHEN "discounts" IS NULL OR btrim("discounts") NOT LIKE '[%' THEN '[]'::jsonb
|
|
74
|
+
ELSE "discounts"::jsonb
|
|
75
|
+
END`
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The price a product sells for right now, for use in an `ORDER BY` over
|
|
79
|
+
* `teleport_products`. Falls back to the list price when no markdown is live.
|
|
80
|
+
*
|
|
81
|
+
* Mirrors `resolveActiveDiscount` + `computeDiscountedPrice`: the FIRST live
|
|
82
|
+
* entry in array order wins (hence `WITH ORDINALITY` and `ORDER BY ord`),
|
|
83
|
+
* unknown types and non-positive values are ignored, a percentage is capped at
|
|
84
|
+
* 100, and the result is floored at zero and rounded to the cent.
|
|
85
|
+
*/
|
|
86
|
+
export const buildProductEffectivePriceSql = (): string =>
|
|
87
|
+
`COALESCE((
|
|
88
|
+
SELECT ROUND(GREATEST(0,
|
|
89
|
+
CASE
|
|
90
|
+
WHEN entry.kind = 'percentage'
|
|
91
|
+
THEN "price"::numeric * (1 - LEAST(entry.amount, 100) / 100)
|
|
92
|
+
ELSE "price"::numeric - entry.amount
|
|
93
|
+
END
|
|
94
|
+
), 2)
|
|
95
|
+
FROM (
|
|
96
|
+
SELECT
|
|
97
|
+
d.value->>'type' AS kind,
|
|
98
|
+
CASE
|
|
99
|
+
WHEN d.value->>'value' ~ '^[0-9]+(\\.[0-9]+)?$' THEN (d.value->>'value')::numeric
|
|
100
|
+
ELSE 0
|
|
101
|
+
END AS amount,
|
|
102
|
+
d.value->>'startsAt' AS starts_at,
|
|
103
|
+
d.value->>'endsAt' AS ends_at,
|
|
104
|
+
d.ord AS ord
|
|
105
|
+
FROM jsonb_array_elements(${SAFE_DISCOUNTS_JSONB}) WITH ORDINALITY AS d(value, ord)
|
|
106
|
+
) AS entry
|
|
107
|
+
WHERE entry.amount > 0
|
|
108
|
+
AND entry.kind IN ('percentage', 'fixed')
|
|
109
|
+
AND (entry.starts_at IS NULL OR entry.starts_at <= ${NOW_AS_ISO_TEXT})
|
|
110
|
+
AND (entry.ends_at IS NULL OR entry.ends_at > ${NOW_AS_ISO_TEXT})
|
|
111
|
+
ORDER BY entry.ord
|
|
112
|
+
LIMIT 1
|
|
113
|
+
), "price"::numeric)`
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* How much a product is marked down RIGHT NOW, as a percentage of its list
|
|
117
|
+
* price — the metric "sort by discount" means.
|
|
118
|
+
*
|
|
119
|
+
* A percentage rather than an absolute saving, because it is the only figure
|
|
120
|
+
* comparable across a mixed catalogue: $20 off a $2000 sofa is a worse deal
|
|
121
|
+
* than $5 off a $10 mug, and sorting by the absolute amount would rank them the
|
|
122
|
+
* other way round.
|
|
123
|
+
*
|
|
124
|
+
* Zero when nothing is live, so undiscounted products collect at one end
|
|
125
|
+
* instead of scattering. `price > 0` is the divide-by-zero guard AND the NULL
|
|
126
|
+
* guard — `NULL > 0` is NULL, which is not true, so a priceless row takes the
|
|
127
|
+
* ELSE branch.
|
|
128
|
+
*/
|
|
129
|
+
export const buildProductDiscountPercentageSql = (): string =>
|
|
130
|
+
`CASE
|
|
131
|
+
WHEN "price"::numeric > 0
|
|
132
|
+
THEN ROUND(((("price"::numeric - ${buildProductEffectivePriceSql()}) / "price"::numeric) * 100), 4)
|
|
133
|
+
ELSE 0
|
|
134
|
+
END`
|
|
135
|
+
|
|
136
|
+
/** Whether a generated fetcher for `tableName` should order prices by the markdown. */
|
|
137
|
+
export const supportsProductPriceSort = (tableName: string): boolean =>
|
|
138
|
+
tableName === PRODUCTS_TABLE_NAME
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The complete `sortFieldSql` helper a SQL fetcher embeds in its generated
|
|
142
|
+
* route.
|
|
143
|
+
*
|
|
144
|
+
* Returned as one ready-made string, and the SQL is emitted through
|
|
145
|
+
* `JSON.stringify` — a double-quoted JS literal carries no backticks and no
|
|
146
|
+
* `${`, so it can be interpolated into a fetcher's template literal without any
|
|
147
|
+
* nested-escaping hazard. (Emitting it inline is how this first went wrong: a
|
|
148
|
+
* backtick inside an explanatory comment closed the enclosing template.)
|
|
149
|
+
*
|
|
150
|
+
* On a table that is not the products table the helper still exists but is the
|
|
151
|
+
* identity, so a fetcher can embed it unconditionally.
|
|
152
|
+
*/
|
|
153
|
+
export const generateSortFieldSqlHelper = (tableName: string): string => {
|
|
154
|
+
const branches = supportsProductPriceSort(tableName)
|
|
155
|
+
? [
|
|
156
|
+
` if (field === ${JSON.stringify(PRODUCT_PRICE_SORT_FIELD)}) {`,
|
|
157
|
+
` return ${JSON.stringify(buildProductEffectivePriceSql())}`,
|
|
158
|
+
` }`,
|
|
159
|
+
` if (field === ${JSON.stringify(PRODUCT_DISCOUNT_SORT_FIELD)}) {`,
|
|
160
|
+
` return ${JSON.stringify(buildProductDiscountPercentageSql())}`,
|
|
161
|
+
` }`,
|
|
162
|
+
].join('\n') + '\n'
|
|
163
|
+
: ''
|
|
164
|
+
|
|
165
|
+
return [
|
|
166
|
+
'// Maps a requested sort field to the SQL that orders by it. Only the',
|
|
167
|
+
"// products table's price and discounts are rewritten - into the price",
|
|
168
|
+
'// customers actually pay today, and into how far each product is marked',
|
|
169
|
+
'// down. Every other field is passed through, so this can never change an',
|
|
170
|
+
'// unrelated sort.',
|
|
171
|
+
'function sortFieldSql(field) {',
|
|
172
|
+
branches + ' return field',
|
|
173
|
+
'}',
|
|
174
|
+
].join('\n')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The plain-column ORDER BY used when the rewritten expression cannot run.
|
|
179
|
+
*
|
|
180
|
+
* Both rewritten fields fall back to `price`, never to themselves: `discounts`
|
|
181
|
+
* is the JSON column, and ordering a page by raw JSON is meaningless — while a
|
|
182
|
+
* store provisioned before discounts shipped has no such column at all, so
|
|
183
|
+
* retrying with it would fail exactly like the expression did.
|
|
184
|
+
*/
|
|
185
|
+
export const generateSortFallbackFieldHelper = (tableName: string): string => {
|
|
186
|
+
const branches = supportsProductPriceSort(tableName)
|
|
187
|
+
? ` if (field === ${JSON.stringify(PRODUCT_PRICE_SORT_FIELD)} || field === ${JSON.stringify(
|
|
188
|
+
PRODUCT_DISCOUNT_SORT_FIELD
|
|
189
|
+
)}) {\n return ${JSON.stringify(PRODUCT_SORT_FALLBACK_COLUMN)}\n }\n`
|
|
190
|
+
: ''
|
|
191
|
+
|
|
192
|
+
return ['function sortFallbackField(field) {', branches + ' return field', '}'].join('\n')
|
|
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' : ''
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
|
-
import { StorefrontTax } from '@teleporthq/teleport-shared'
|
|
2
|
+
import { ProductDiscounts, StorefrontTax } from '@teleporthq/teleport-shared'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Everything the product transform needs baked in at export time, beyond the
|
|
@@ -14,6 +14,18 @@ export interface EcommerceProductTransformOptions {
|
|
|
14
14
|
* Resolved by the caller through `StorefrontTax.resolveStorefrontTaxRate`.
|
|
15
15
|
*/
|
|
16
16
|
storefrontTaxRate?: number
|
|
17
|
+
/**
|
|
18
|
+
* TRUE when stock levels must never gate purchasability in the storefront:
|
|
19
|
+
* the merchant disabled stock management entirely OR opted into backorders
|
|
20
|
+
* (`stockManagementConfig.allowBackorders`). Resolved by the caller through
|
|
21
|
+
* `buildProductTransformOptions`. Baked into the generated transform as
|
|
22
|
+
* `ALLOW_BACKORDERS`: `outOfStock` then always reads 'false' and every
|
|
23
|
+
* EXISTING variant combination counts as in stock — a combination that does
|
|
24
|
+
* not exist at all stays unavailable (there is nothing to backorder).
|
|
25
|
+
* Mirrors the GUI rule in
|
|
26
|
+
* `apps/gui/app/project-page/features/e-commerce/utils/product-variants.ts`.
|
|
27
|
+
*/
|
|
28
|
+
allowBackorders?: boolean
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
/**
|
|
@@ -69,9 +81,16 @@ export const generateEcommerceProductTransformationCode = (
|
|
|
69
81
|
const taxHelperCode = StorefrontTax.generateStorefrontTaxHelperCode(
|
|
70
82
|
options.storefrontTaxRate ?? 0
|
|
71
83
|
)
|
|
84
|
+
const allowBackordersLiteral = options.allowBackorders === true ? 'true' : 'false'
|
|
72
85
|
|
|
73
86
|
return `
|
|
74
87
|
${taxHelperCode}
|
|
88
|
+
${ProductDiscounts.generateProductDiscountHelperCode()}
|
|
89
|
+
|
|
90
|
+
// Baked from the merchant's stock settings: TRUE when stock never blocks a
|
|
91
|
+
// purchase (stock management off OR backorders allowed). Regenerating the
|
|
92
|
+
// project refreshes it — do not hand-edit. See EcommerceProductTransformOptions.
|
|
93
|
+
var ALLOW_BACKORDERS = ${allowBackordersLiteral}
|
|
75
94
|
|
|
76
95
|
// Category taxonomy (id -> {name, slug, translations}), baked in at export
|
|
77
96
|
// time — see flattenCategoriesById in ecommerce-product.ts. Empty when the
|
|
@@ -90,6 +109,50 @@ function resolveCategoryName(categoryInfo, currentLang, mainLang) {
|
|
|
90
109
|
return categoryInfo.name
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
// The merchant's "Show 3D model by default" toggle, as every backend may hand
|
|
113
|
+
// it back: a real boolean, 1/0, 'true'/'false'/'t'/'f' strings — and NULL /
|
|
114
|
+
// absent / '' as ON, because a product that has a model shows it unless the
|
|
115
|
+
// merchant switched it off. MUST mirror normalizeShowModelFlag in the GUI's
|
|
116
|
+
// features/e-commerce/constants/product-model.ts.
|
|
117
|
+
function normalizeShowModelFlag(raw) {
|
|
118
|
+
if (raw === null || raw === undefined || raw === '') return true
|
|
119
|
+
if (typeof raw === 'boolean') return raw
|
|
120
|
+
if (typeof raw === 'number') return raw !== 0
|
|
121
|
+
if (typeof raw === 'string') {
|
|
122
|
+
var token = raw.trim().toLowerCase()
|
|
123
|
+
return !(token === 'false' || token === 'f' || token === '0' || token === 'no' || token === 'off')
|
|
124
|
+
}
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The product's media gallery in display order, HERO FIRST: the model when it
|
|
129
|
+
// is shown by default, otherwise the images with the model last. Each entry is
|
|
130
|
+
// { kind: 'image' | 'model', src, poster, thumbnail, alt }; 'thumbnail' is what
|
|
131
|
+
// a strip draws and is never empty for an entry that can appear in one. MUST
|
|
132
|
+
// mirror buildProductGalleryMedia in the GUI's
|
|
133
|
+
// features/e-commerce/utils/product-gallery-media.ts.
|
|
134
|
+
function buildProductGalleryMedia(params) {
|
|
135
|
+
var imageItems = []
|
|
136
|
+
for (var gi = 0; gi < params.images.length; gi++) {
|
|
137
|
+
imageItems.push({
|
|
138
|
+
kind: 'image',
|
|
139
|
+
src: params.images[gi],
|
|
140
|
+
poster: '',
|
|
141
|
+
thumbnail: params.images[gi],
|
|
142
|
+
alt: params.imageAlt || params.name,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
if (!params.modelUrl) return imageItems
|
|
146
|
+
var modelItem = {
|
|
147
|
+
kind: 'model',
|
|
148
|
+
src: params.modelUrl,
|
|
149
|
+
poster: params.modelPoster || '',
|
|
150
|
+
thumbnail: params.modelPoster || params.mainImage || params.images[0] || '',
|
|
151
|
+
alt: params.name,
|
|
152
|
+
}
|
|
153
|
+
return params.showModel === 'true' ? [modelItem].concat(imageItems) : imageItems.concat([modelItem])
|
|
154
|
+
}
|
|
155
|
+
|
|
93
156
|
// ============================================================
|
|
94
157
|
// E-Commerce Product Transformation
|
|
95
158
|
// ============================================================
|
|
@@ -315,6 +378,36 @@ function buildEcommerceProduct(record, options) {
|
|
|
315
378
|
// Step 7: images = full resolved deduplicated array
|
|
316
379
|
var images = resolvedImages
|
|
317
380
|
|
|
381
|
+
// -------------------------------------------------------
|
|
382
|
+
// 3D model (one per product; GUI: constants/product-model.ts)
|
|
383
|
+
// -------------------------------------------------------
|
|
384
|
+
// model_url / model_poster_url hold an asset id OR a raw URL like image_url.
|
|
385
|
+
// A poster the merchant never got (capture failed, plan cap) falls back to
|
|
386
|
+
// the main image so <model-viewer> always has something to paint first.
|
|
387
|
+
var modelUrl = resolveMediaUrl(record.model_url, assetMap)
|
|
388
|
+
var modelPoster = modelUrl ? resolveMediaUrl(record.model_poster_url, assetMap) || mainImage : null
|
|
389
|
+
var hasModel = modelUrl ? 'true' : 'false'
|
|
390
|
+
// STRING gate ('true'/'false') like outOfStock: whether the model replaces the
|
|
391
|
+
// main image on the card / details hero. A product whose only media is the
|
|
392
|
+
// model always shows it. An absent field must read as the IMAGE branch, so the
|
|
393
|
+
// builders gate the viewer on = 'true' and the image on != 'true'.
|
|
394
|
+
var showModel =
|
|
395
|
+
modelUrl && (normalizeShowModelFlag(record.show_model_by_default) || images.length === 0)
|
|
396
|
+
? 'true'
|
|
397
|
+
: 'false'
|
|
398
|
+
var galleryMedia = buildProductGalleryMedia({
|
|
399
|
+
images: images,
|
|
400
|
+
mainImage: mainImage,
|
|
401
|
+
modelUrl: modelUrl,
|
|
402
|
+
modelPoster: modelPoster,
|
|
403
|
+
showModel: showModel,
|
|
404
|
+
imageAlt: imageAlt,
|
|
405
|
+
name: name,
|
|
406
|
+
})
|
|
407
|
+
// Everything that is not the hero — the details thumbnail strip. When the
|
|
408
|
+
// model is the hero the main image moves in here, so the modal stays reachable.
|
|
409
|
+
var galleryThumbnails = galleryMedia.slice(1)
|
|
410
|
+
|
|
318
411
|
// Timestamps
|
|
319
412
|
var rawCreatedAt = pickFirst(record.created_at, record.createdAt, record.created)
|
|
320
413
|
var createdAt = normalizeTimestamp(rawCreatedAt)
|
|
@@ -332,7 +425,9 @@ function buildEcommerceProduct(record, options) {
|
|
|
332
425
|
// string), leaving the Add to Cart button visible on out-of-stock
|
|
333
426
|
// products. NULL/NaN quantity → 'false' (treated as "unlimited
|
|
334
427
|
// stock", same as the upstream cart-availability rewriter).
|
|
335
|
-
|
|
428
|
+
// With ALLOW_BACKORDERS the merchant sells past zero (or does not track
|
|
429
|
+
// stock at all), so nothing is ever presented as out of stock.
|
|
430
|
+
var outOfStock = (!ALLOW_BACKORDERS && quantity !== null && !isNaN(quantity) && quantity <= 0) ? 'true' : 'false'
|
|
336
431
|
|
|
337
432
|
// Per-value availability + default-selection flags, derived from the
|
|
338
433
|
// purchasable combinations, so the storefront picker renders its
|
|
@@ -378,24 +473,45 @@ function buildEcommerceProduct(record, options) {
|
|
|
378
473
|
// gross, because nothing writes them back.
|
|
379
474
|
//
|
|
380
475
|
// MUST mirror packages/renderer/src/utils/ecommerce-products.ts.
|
|
381
|
-
|
|
476
|
+
// The scheduled markdown in force right now, resolved from the raw column on
|
|
477
|
+
// every read so a discount starts and expires on its own. It comes off the NET
|
|
478
|
+
// price BEFORE tax: tax is a percentage of what is actually charged.
|
|
479
|
+
var activeDiscount = __pdResolveActive(record.discounts, Date.now())
|
|
480
|
+
var displayPrice = grossMoney(__pdDiscountedPrice(price, activeDiscount))
|
|
481
|
+
var defaultVariantNetPrice = firstVariant
|
|
482
|
+
? safeNumber(formatVariantPrice(firstVariant.price, price), 0)
|
|
483
|
+
: 0
|
|
382
484
|
var defaultVariantDisplayPrice = firstVariant
|
|
383
|
-
? grossMoney(
|
|
485
|
+
? grossMoney(__pdDiscountedPrice(defaultVariantNetPrice, activeDiscount))
|
|
384
486
|
: ''
|
|
385
487
|
var variantsDisplay = []
|
|
386
488
|
for (var dv = 0; dv < variants.length; dv++) {
|
|
387
489
|
var dvRow = variants[dv]
|
|
490
|
+
// A null variant price INHERITS the product price, so discount and gross the
|
|
491
|
+
// RESOLVED value — otherwise an inheriting combination would fall back to
|
|
492
|
+
// the net base price in the picker while an overriding one showed gross.
|
|
493
|
+
var dvNetPrice = dvRow.price == null ? price : dvRow.price
|
|
388
494
|
variantsDisplay.push({
|
|
389
495
|
id: dvRow.id,
|
|
390
496
|
options: dvRow.options,
|
|
391
|
-
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
497
|
+
price: applyStorefrontTax(__pdDiscountedPrice(dvNetPrice, activeDiscount)),
|
|
498
|
+
// What this combination costs WITHOUT the discount, for the struck-through
|
|
499
|
+
// price beside it. Null when nothing is discounted, so the picker's click
|
|
500
|
+
// resolver can tell "no saving" from "a saving of zero".
|
|
501
|
+
originalPrice: activeDiscount ? applyStorefrontTax(dvNetPrice) : null,
|
|
395
502
|
quantity: dvRow.quantity,
|
|
396
503
|
image_url: dvRow.image_url,
|
|
397
504
|
})
|
|
398
505
|
}
|
|
506
|
+
// Discount display fields. Strings throughout: rendering conditions compare
|
|
507
|
+
// operands as strings, and a bound text node prints its value verbatim.
|
|
508
|
+
var hasDiscount = activeDiscount ? 'true' : 'false'
|
|
509
|
+
var discountLabel = activeDiscount
|
|
510
|
+
? __pdDiscountLabel(activeDiscount, currencySymbol, getCurrencyInfo(currency).position)
|
|
511
|
+
: ''
|
|
512
|
+
var originalDisplayPrice = activeDiscount ? grossMoney(price) : ''
|
|
513
|
+
var originalDefaultVariantDisplayPrice =
|
|
514
|
+
activeDiscount && firstVariant ? grossMoney(defaultVariantNetPrice) : ''
|
|
399
515
|
// Can this product be added to the cart at all, before the shopper touches the
|
|
400
516
|
// picker? A STRING, same convention as outOfStock/requiresVariantSelection.
|
|
401
517
|
//
|
|
@@ -457,6 +573,12 @@ function buildEcommerceProduct(record, options) {
|
|
|
457
573
|
image_url: mainImage,
|
|
458
574
|
galleryImages: galleryImages,
|
|
459
575
|
images: images,
|
|
576
|
+
modelUrl: modelUrl,
|
|
577
|
+
modelPoster: modelPoster,
|
|
578
|
+
hasModel: hasModel,
|
|
579
|
+
showModel: showModel,
|
|
580
|
+
galleryMedia: galleryMedia,
|
|
581
|
+
galleryThumbnails: galleryThumbnails,
|
|
460
582
|
createdAt: createdAt,
|
|
461
583
|
updatedAt: updatedAt,
|
|
462
584
|
created: created,
|
|
@@ -471,14 +593,23 @@ function buildEcommerceProduct(record, options) {
|
|
|
471
593
|
defaultVariantPrice: defaultVariantPrice,
|
|
472
594
|
displayPrice: displayPrice,
|
|
473
595
|
defaultVariantDisplayPrice: defaultVariantDisplayPrice,
|
|
596
|
+
hasDiscount: hasDiscount,
|
|
597
|
+
discountLabel: discountLabel,
|
|
598
|
+
originalDisplayPrice: originalDisplayPrice,
|
|
599
|
+
originalDefaultVariantDisplayPrice: originalDefaultVariantDisplayPrice,
|
|
474
600
|
hasPurchasableVariant: hasPurchasableVariant,
|
|
475
601
|
}
|
|
476
602
|
}
|
|
477
603
|
|
|
478
604
|
// --- Variant availability helpers (mirror packages/renderer ecommerce-products) ---
|
|
479
|
-
// A variant is in stock when its quantity is null (unlimited) or > 0.
|
|
605
|
+
// A variant is in stock when its quantity is null (unlimited) or > 0. With
|
|
606
|
+
// ALLOW_BACKORDERS every EXISTING combination is purchasable regardless of its
|
|
607
|
+
// quantity — but a missing combination (null) is still not in stock: there is
|
|
608
|
+
// no variant row to backorder.
|
|
480
609
|
function isVariantInStock(v) {
|
|
481
|
-
|
|
610
|
+
if (!v) return false
|
|
611
|
+
if (ALLOW_BACKORDERS) return true
|
|
612
|
+
return v.quantity == null || v.quantity > 0
|
|
482
613
|
}
|
|
483
614
|
function variantCoversAllAxes(v, axisKeys) {
|
|
484
615
|
if (!v || !v.options) return false
|
|
@@ -9,19 +9,37 @@ import {
|
|
|
9
9
|
|
|
10
10
|
export type { EcommerceProductTransformOptions }
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Stock never gates purchasability when the merchant disabled stock management
|
|
14
|
+
* entirely or opted into backorders. Mirrors the GUI rule in
|
|
15
|
+
* `apps/gui/app/project-page/features/e-commerce/utils/product-variants.ts`
|
|
16
|
+
* (`isVariantInStock`). A project without e-commerce settings keeps the
|
|
17
|
+
* strict default (stock gates apply) — same behaviour as before the flag.
|
|
18
|
+
*/
|
|
19
|
+
const resolveAllowBackorders = (
|
|
20
|
+
settings: GeneratorOptions['ecommerceSettings'] | undefined
|
|
21
|
+
): boolean => {
|
|
22
|
+
if (!settings) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
return !settings.stockManagement || settings.stockManagementConfig?.allowBackorders === true
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Collects everything the product transform bakes in from the generator
|
|
14
30
|
* options, in ONE place so every fetcher-emitting call site stays in step.
|
|
15
31
|
*
|
|
16
32
|
* `storefrontTaxRate` is what turns a NET catalogue price into the price the
|
|
17
33
|
* shopper is quoted; `resolveStorefrontTaxRate` returns 0 (a no-op) for
|
|
18
|
-
* tax-inclusive or untaxed stores.
|
|
34
|
+
* tax-inclusive or untaxed stores. `allowBackorders` is the effective
|
|
35
|
+
* "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
|
|
19
36
|
*/
|
|
20
37
|
export const buildProductTransformOptions = (
|
|
21
38
|
options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings'>
|
|
22
39
|
): EcommerceProductTransformOptions => ({
|
|
23
40
|
categories: options.ecommerceSettings?.categories,
|
|
24
41
|
storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
|
|
42
|
+
allowBackorders: resolveAllowBackorders(options.ecommerceSettings),
|
|
25
43
|
})
|
|
26
44
|
|
|
27
45
|
export type TransformationType = 'blog-post' | 'ecommerce-product' | null
|
|
@@ -234,6 +234,20 @@ function resolveAssetUrl(value, assetMap) {
|
|
|
234
234
|
return value
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// Whether an asset-id resolution produced a real URL. resolveAssetUrl returns
|
|
238
|
+
// an UNKNOWN id verbatim (asset deleted, teleport_assets not mirrored yet) and
|
|
239
|
+
// a bare 'TQ_...' must never reach a <model-viewer src> or a poster.
|
|
240
|
+
function isResolvedMediaUrl(value) {
|
|
241
|
+
return typeof value === 'string' && (isUrl(value) || value.charAt(0) === '/')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// resolveAssetUrl for MEDIA that has no safe fallback: a value that did not
|
|
245
|
+
// resolve to a URL is reported as absent (null) instead of passed through.
|
|
246
|
+
function resolveMediaUrl(value, assetMap) {
|
|
247
|
+
var resolved = resolveAssetUrl(value, assetMap)
|
|
248
|
+
return isResolvedMediaUrl(resolved) ? resolved : null
|
|
249
|
+
}
|
|
250
|
+
|
|
237
251
|
function resolveAssetUrls(values, assetMap) {
|
|
238
252
|
if (!Array.isArray(values)) return []
|
|
239
253
|
return values
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ArrayMapperRegistry - Centralized registry for managing array mappers,
|
|
3
|
-
* their associated states, search inputs, and pagination buttons.
|
|
4
|
-
*
|
|
5
|
-
* This ensures consistent index usage across:
|
|
6
|
-
* - Component state declarations
|
|
7
|
-
* - DataProvider params
|
|
8
|
-
* - getStaticProps fetch calls and prop names
|
|
9
|
-
* - Search input bindings
|
|
10
|
-
* - Pagination button bindings
|
|
11
|
-
*/
|
|
12
|
-
export type MapperType = 'paginated' | 'pagination-only' | 'search-only' | 'plain';
|
|
13
|
-
export interface ArrayMapperRegistryEntry {
|
|
14
|
-
registryIndex: number;
|
|
15
|
-
type: MapperType;
|
|
16
|
-
dataSourceIdentifier: string;
|
|
17
|
-
arrayMapperRenderProp: string;
|
|
18
|
-
uidlGlobalIndex: number;
|
|
19
|
-
paginated: boolean;
|
|
20
|
-
searchEnabled: boolean;
|
|
21
|
-
perPage: number;
|
|
22
|
-
searchDebounce: number;
|
|
23
|
-
searchDefaultValue?: string;
|
|
24
|
-
queryColumns: string[];
|
|
25
|
-
stateVarPrefix: string;
|
|
26
|
-
pageStateVar?: string;
|
|
27
|
-
setPageStateVar?: string;
|
|
28
|
-
maxPagesStateVar?: string;
|
|
29
|
-
setMaxPagesStateVar?: string;
|
|
30
|
-
combinedStateVar?: string;
|
|
31
|
-
setCombinedStateVar?: string;
|
|
32
|
-
searchQueryVar?: string;
|
|
33
|
-
setSearchQueryVar?: string;
|
|
34
|
-
debouncedSearchQueryVar?: string;
|
|
35
|
-
setDebouncedSearchQueryVar?: string;
|
|
36
|
-
skipDebounceRefVar?: string;
|
|
37
|
-
getStaticPropsDataVar: string;
|
|
38
|
-
getStaticPropsMaxPagesVar: string;
|
|
39
|
-
getStaticPropsPropName: string;
|
|
40
|
-
getStaticPropsMaxPagesPropName: string;
|
|
41
|
-
searchInputClass?: string;
|
|
42
|
-
paginationNodeClass?: string;
|
|
43
|
-
prevButtonClass?: string;
|
|
44
|
-
nextButtonClass?: string;
|
|
45
|
-
dataProviderJSX?: any;
|
|
46
|
-
searchInputJSX?: any;
|
|
47
|
-
}
|
|
48
|
-
export declare class ArrayMapperRegistry {
|
|
49
|
-
private entries;
|
|
50
|
-
private paginatedCount;
|
|
51
|
-
private paginationOnlyCount;
|
|
52
|
-
private searchOnlyCount;
|
|
53
|
-
private plainCount;
|
|
54
|
-
registerFromUIDL(config: {
|
|
55
|
-
dataSourceIdentifier: string;
|
|
56
|
-
arrayMapperRenderProp: string;
|
|
57
|
-
uidlGlobalIndex: number;
|
|
58
|
-
paginated: boolean;
|
|
59
|
-
searchEnabled: boolean;
|
|
60
|
-
perPage?: number;
|
|
61
|
-
searchDebounce?: number;
|
|
62
|
-
searchDefaultValue?: string;
|
|
63
|
-
queryColumns?: string[];
|
|
64
|
-
}): ArrayMapperRegistryEntry;
|
|
65
|
-
getEntry(registryIndex: number): ArrayMapperRegistryEntry | undefined;
|
|
66
|
-
getEntryByUIDLIndex(uidlGlobalIndex: number): ArrayMapperRegistryEntry | undefined;
|
|
67
|
-
getEntriesByType(type: MapperType): ArrayMapperRegistryEntry[];
|
|
68
|
-
getAllEntries(): ArrayMapperRegistryEntry[];
|
|
69
|
-
getPaginatedEntries(): ArrayMapperRegistryEntry[];
|
|
70
|
-
getPaginationOnlyEntries(): ArrayMapperRegistryEntry[];
|
|
71
|
-
getSearchOnlyEntries(): ArrayMapperRegistryEntry[];
|
|
72
|
-
getPlainEntries(): ArrayMapperRegistryEntry[];
|
|
73
|
-
updateJSXReferences(registryIndex: number, updates: Partial<Pick<ArrayMapperRegistryEntry, 'dataProviderJSX' | 'searchInputJSX' | 'searchInputClass' | 'paginationNodeClass' | 'prevButtonClass' | 'nextButtonClass'>>): void;
|
|
74
|
-
getStateInitializationMaxPagesPropName(entry: ArrayMapperRegistryEntry): string;
|
|
75
|
-
toDebugSummary(): string;
|
|
76
|
-
private determineType;
|
|
77
|
-
private getTypeIndex;
|
|
78
|
-
private incrementTypeCount;
|
|
79
|
-
private generateStateVarPrefix;
|
|
80
|
-
private generateStateVarNames;
|
|
81
|
-
private generateGetStaticPropsVarNames;
|
|
82
|
-
private capitalize;
|
|
83
|
-
}
|
|
84
|
-
export declare function createRegistryFromUIDL(uidlNode: any, allResources: any): ArrayMapperRegistry;
|
|
85
|
-
//# sourceMappingURL=array-mapper-registry.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"array-mapper-registry.d.ts","sourceRoot":"","sources":["../../src/array-mapper-registry.ts"],"names":[],"mappings":"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"}
|