@base44/app-plugin-commerce 0.10.0 → 0.10.1
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/package.json +1 -1
- package/skills/commerce/installation/install.md +3 -2
- package/skills/commerce/references/storefront-ui.md +1 -0
- package/src/commerce/storefront-ui/CheckoutPage.jsx +19 -2
- package/src/commerce/storefront-ui/i18n/locales/de.js +13 -0
- package/src/commerce/storefront-ui/i18n/locales/en.js +13 -0
- package/src/commerce/storefront-ui/i18n/locales/es.js +13 -0
- package/src/commerce/storefront-ui/i18n/locales/fr.js +13 -0
- package/src/commerce/storefront-ui/i18n/locales/ja.js +13 -0
- package/src/commerce/storefront-ui/i18n/locales/pt.js +13 -0
- package/src/commerce/storefront-ui/storefront-ui.css +6 -2
- package/src/commerce/utils/price.js +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
|
@@ -210,7 +210,7 @@ Everything below is already unwrapped — no `.data`, no envelope. `formatMoney`
|
|
|
210
210
|
| `useProduct(slug)` | `{ status, product, view, price, selection, pick, quantity, setQuantity, incQuantity, decQuantity, maxQuantity, canIncrease, categories, ribbons, upsells, crossSells, reviews, reload }` — `status`: `"loading" \| "ready" \| "not_found" \| "error"`; `reviews` is `{ items, page, per_page, has_next }`. |
|
|
211
211
|
| `useAddToCart(p)` | `{ state, disabled, addToCart, adding, error, soldOut, needsSelection, quantity, increase, decrease, canIncrease, canDecrease, showQuantity, reset }` — `state`: `"ready" \| "adding" \| "sold_out" \| "needs_selection"`. |
|
|
212
212
|
| `variantAxes(view, pick)` | `[{ key, name, selectedOption, options: [{ value, selected, disabled, outOfStock, pick }] }]`. |
|
|
213
|
-
| `productPrice(rowOrView, { formatMoney })` | `{ label, compareAtLabel, onSale, isFrom, isRange, min, max }` — `label` is what to render. |
|
|
213
|
+
| `productPrice(rowOrView, { formatMoney, fromLabel })` | `{ label, compareAtLabel, onSale, isFrom, isRange, min, max }` — `label` is what to render. ⚑ `fromLabel` defaults to English "From": a store in another language passes its own word once, or every variant card ships one English word. |
|
|
214
214
|
| `productImages(product)` | `[{ src, name, alt }]`, de-duplicated. `[]` is legitimate → render your placeholder. |
|
|
215
215
|
| `productRibbons(product)` | `[{ id, name }]` — **objects**, and the field can be absent; takes a listing row or `useProduct().product`. |
|
|
216
216
|
| `productSpecs(product)` | `[{ key, label, titleLabel, value, type, number, unit, items }]` from `meta_data` — `type` is `"numeric" \| "duration" \| "location" \| "list" \| "text"`, inferred, with `number`/`unit` split out for the first two and `items` for a list. `findSpec(rows, key)` looks one up ignoring case/spaces/`_`/`-`. Never match on `label` — meta keys are free text. |
|
|
@@ -227,7 +227,7 @@ import { useProductList, useCategories, useStoreInfo, useFormatMoney, productPri
|
|
|
227
227
|
|
|
228
228
|
⚑ **Render paging whenever `hasNext` is true** — `{list.hasNext && <button type="button" onClick={list.next} disabled={list.busy}>…</button>}` (append mode: `list.loadMore`); a page that renders nothing for paging ships a catalog silently capped at `per_page`. Drive filters from `useCategories()`/`useRibbons()` data via `setParams`, never from hardcoded names — a renamed ribbon must not strand a dead button.
|
|
229
229
|
|
|
230
|
-
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length, ABV — read off `productSpecs(row)`.
|
|
230
|
+
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price; on a non-English store pass `fromLabel` with the store's word), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length, ABV — read off `productSpecs(row)`.
|
|
231
231
|
|
|
232
232
|
⚑ **Ribbons belong in both views** — grid and product page. They are the merchant's own merchandising ("Limited", "Last pieces"), and each links to its filtered listing (`/collection?ribbon_id=<id>`). `productRibbons(row)` hands you `{id, name}` **objects** — render `r.name`, key the link on `r.id`; the entry itself in JSX is React's "Objects are not valid as a React child". Never render a bare "Ribbons:" label with nothing after it. ⚑ **A ribbon link inside a card that is itself a link nests `<a>` in `<a>`** — invalid, React warns. In the grid use plain labels, or link the image and title rather than the whole card; keep ribbon links on the product page.
|
|
233
233
|
|
|
@@ -336,6 +336,7 @@ The components style themselves; with no block at all they follow the app's shad
|
|
|
336
336
|
--sfui-border: #e5e0d5;
|
|
337
337
|
--sfui-radius: 0.5rem; /* panels; controls derive from it */
|
|
338
338
|
--sfui-font-heading: "Fraunces", serif; /* body text inherits the site's font by itself */
|
|
339
|
+
/* optional: --sfui-font-label — only if the site's labels/microcopy use a second face */
|
|
339
340
|
}
|
|
340
341
|
```
|
|
341
342
|
|
|
@@ -37,6 +37,7 @@ then to safe literals — an app with no block at all is already correct.
|
|
|
37
37
|
| `--sfui-border` | hairlines, control borders | `--border` |
|
|
38
38
|
| `--sfui-radius` | panels/cards (controls derive ×0.6, or set `--sfui-radius-control`) | `--radius` → `0.75rem` |
|
|
39
39
|
| `--sfui-font-heading` | headings only — body text always inherits the site font | `inherit` |
|
|
40
|
+
| `--sfui-font-label` | section/field labels and microcopy — set only when the site gives labels their own face (e.g. a mono label idiom) | `inherit` |
|
|
40
41
|
|
|
41
42
|
**Contrast guard**: on mount each active pair is measured; a pair under 3:1 is reset
|
|
42
43
|
to its default (both sides) and a `console.warn` names it. So a "my colors aren't
|
|
@@ -130,6 +130,23 @@ function CheckoutBody({ brand, layout, sections, continueHref, onPlaced }) {
|
|
|
130
130
|
|
|
131
131
|
const disabled = !canPlaceOrder || placing || (termsCheckbox && !termsAccepted);
|
|
132
132
|
|
|
133
|
+
// AddressFields' built-in labels are English; the shipped checkout localizes
|
|
134
|
+
// them like every other label on the page.
|
|
135
|
+
const addressLabels = {
|
|
136
|
+
first_name: tt("checkout.address.first_name"),
|
|
137
|
+
last_name: tt("checkout.address.last_name"),
|
|
138
|
+
email: tt("checkout.address.email"),
|
|
139
|
+
company: tt("checkout.address.company"),
|
|
140
|
+
address_1: tt("checkout.address.address_1"),
|
|
141
|
+
address_2: tt("checkout.address.address_2"),
|
|
142
|
+
country: tt("checkout.address.country"),
|
|
143
|
+
city: tt("checkout.address.city"),
|
|
144
|
+
state: tt("checkout.address.state"),
|
|
145
|
+
postcode: tt("checkout.address.postcode"),
|
|
146
|
+
phone: tt(phone === "required" ? "checkout.address.phone" : "checkout.address.phoneOptional"),
|
|
147
|
+
};
|
|
148
|
+
const selectPlaceholder = tt("checkout.address.select");
|
|
149
|
+
|
|
133
150
|
return (
|
|
134
151
|
<SfuiRoot className="sfui-checkout" aria-labelledby="sfui-checkout-title">
|
|
135
152
|
<div className="sfui-checkout-inner" data-layout={layout}>
|
|
@@ -140,7 +157,7 @@ function CheckoutBody({ brand, layout, sections, continueHref, onPlaced }) {
|
|
|
140
157
|
|
|
141
158
|
<section className="sfui-section sfui-address" aria-label={tt("checkout.contactTitle")}>
|
|
142
159
|
<h2 className="sfui-label">{tt("checkout.contactTitle")}</h2>
|
|
143
|
-
<AddressFields which="billing" includePhone={phone !== "hidden"} />
|
|
160
|
+
<AddressFields which="billing" includePhone={phone !== "hidden"} labels={addressLabels} selectPlaceholder={selectPlaceholder} />
|
|
144
161
|
{allowsShipToDifferent(shipToDifferent) && (
|
|
145
162
|
<label className="sfui-checkbox">
|
|
146
163
|
<input
|
|
@@ -154,7 +171,7 @@ function CheckoutBody({ brand, layout, sections, continueHref, onPlaced }) {
|
|
|
154
171
|
{checkout.shipToDifferent && (
|
|
155
172
|
<>
|
|
156
173
|
<h2 className="sfui-label">{tt("checkout.shippingAddressTitle")}</h2>
|
|
157
|
-
<AddressFields which="shipping" includePhone={false} />
|
|
174
|
+
<AddressFields which="shipping" includePhone={false} labels={addressLabels} selectPlaceholder={selectPlaceholder} />
|
|
158
175
|
</>
|
|
159
176
|
)}
|
|
160
177
|
</section>
|
|
@@ -60,6 +60,19 @@ export default {
|
|
|
60
60
|
"checkout.hint.none_available": "An diese Adresse können wir leider noch nicht liefern.",
|
|
61
61
|
"checkout.hint.syncing": "Lieferoptionen werden aktualisiert…",
|
|
62
62
|
"checkout.hint.payment_none": "Die Kasse ist vorübergehend nicht verfügbar — keine Zahlungsart aktiviert.",
|
|
63
|
+
"checkout.address.first_name": "Vorname",
|
|
64
|
+
"checkout.address.last_name": "Nachname",
|
|
65
|
+
"checkout.address.email": "E-Mail",
|
|
66
|
+
"checkout.address.company": "Firma",
|
|
67
|
+
"checkout.address.address_1": "Adresse",
|
|
68
|
+
"checkout.address.address_2": "Adresszusatz (optional)",
|
|
69
|
+
"checkout.address.country": "Land",
|
|
70
|
+
"checkout.address.city": "Stadt",
|
|
71
|
+
"checkout.address.state": "Bundesland / Region",
|
|
72
|
+
"checkout.address.postcode": "Postleitzahl",
|
|
73
|
+
"checkout.address.phone": "Telefon",
|
|
74
|
+
"checkout.address.phoneOptional": "Telefon (optional)",
|
|
75
|
+
"checkout.address.select": "Bitte wählen…",
|
|
63
76
|
"checkout.blocker.cart_loading": "Ihr Warenkorb wird geladen…",
|
|
64
77
|
"checkout.blocker.empty_cart": "Ihr Warenkorb ist leer.",
|
|
65
78
|
"checkout.blocker.billing_incomplete": "Füllen Sie die erforderlichen Kontaktfelder aus.",
|
|
@@ -70,6 +70,19 @@ export default {
|
|
|
70
70
|
"checkout.hint.none_available": "We can't deliver to that address yet.",
|
|
71
71
|
"checkout.hint.syncing": "Updating delivery options…",
|
|
72
72
|
"checkout.hint.payment_none": "Checkout is temporarily unavailable — no payment method is enabled.",
|
|
73
|
+
"checkout.address.first_name": "First name",
|
|
74
|
+
"checkout.address.last_name": "Last name",
|
|
75
|
+
"checkout.address.email": "Email",
|
|
76
|
+
"checkout.address.company": "Company",
|
|
77
|
+
"checkout.address.address_1": "Address",
|
|
78
|
+
"checkout.address.address_2": "Apartment, suite (optional)",
|
|
79
|
+
"checkout.address.country": "Country",
|
|
80
|
+
"checkout.address.city": "City",
|
|
81
|
+
"checkout.address.state": "State / Region",
|
|
82
|
+
"checkout.address.postcode": "Postal code",
|
|
83
|
+
"checkout.address.phone": "Phone",
|
|
84
|
+
"checkout.address.phoneOptional": "Phone (optional)",
|
|
85
|
+
"checkout.address.select": "Select…",
|
|
73
86
|
"checkout.blocker.cart_loading": "Loading your cart…",
|
|
74
87
|
"checkout.blocker.empty_cart": "Your cart is empty.",
|
|
75
88
|
"checkout.blocker.billing_incomplete": "Fill in the required contact fields.",
|
|
@@ -60,6 +60,19 @@ export default {
|
|
|
60
60
|
"checkout.hint.none_available": "Aún no podemos hacer entregas en esa dirección.",
|
|
61
61
|
"checkout.hint.syncing": "Actualizando opciones de entrega…",
|
|
62
62
|
"checkout.hint.payment_none": "El pago no está disponible por el momento — no hay ningún método de pago habilitado.",
|
|
63
|
+
"checkout.address.first_name": "Nombre",
|
|
64
|
+
"checkout.address.last_name": "Apellido",
|
|
65
|
+
"checkout.address.email": "Correo electrónico",
|
|
66
|
+
"checkout.address.company": "Empresa",
|
|
67
|
+
"checkout.address.address_1": "Dirección",
|
|
68
|
+
"checkout.address.address_2": "Departamento, interior (opcional)",
|
|
69
|
+
"checkout.address.country": "País",
|
|
70
|
+
"checkout.address.city": "Ciudad",
|
|
71
|
+
"checkout.address.state": "Estado / Región",
|
|
72
|
+
"checkout.address.postcode": "Código postal",
|
|
73
|
+
"checkout.address.phone": "Teléfono",
|
|
74
|
+
"checkout.address.phoneOptional": "Teléfono (opcional)",
|
|
75
|
+
"checkout.address.select": "Selecciona…",
|
|
63
76
|
"checkout.blocker.cart_loading": "Cargando tu carrito…",
|
|
64
77
|
"checkout.blocker.empty_cart": "Tu carrito está vacío.",
|
|
65
78
|
"checkout.blocker.billing_incomplete": "Completa los campos de contacto obligatorios.",
|
|
@@ -60,6 +60,19 @@ export default {
|
|
|
60
60
|
"checkout.hint.none_available": "Nous ne livrons pas encore à cette adresse.",
|
|
61
61
|
"checkout.hint.syncing": "Mise à jour des options de livraison…",
|
|
62
62
|
"checkout.hint.payment_none": "Le paiement est temporairement indisponible — aucun moyen de paiement n'est activé.",
|
|
63
|
+
"checkout.address.first_name": "Prénom",
|
|
64
|
+
"checkout.address.last_name": "Nom",
|
|
65
|
+
"checkout.address.email": "E-mail",
|
|
66
|
+
"checkout.address.company": "Société",
|
|
67
|
+
"checkout.address.address_1": "Adresse",
|
|
68
|
+
"checkout.address.address_2": "Appartement, bâtiment (facultatif)",
|
|
69
|
+
"checkout.address.country": "Pays",
|
|
70
|
+
"checkout.address.city": "Ville",
|
|
71
|
+
"checkout.address.state": "État / Région",
|
|
72
|
+
"checkout.address.postcode": "Code postal",
|
|
73
|
+
"checkout.address.phone": "Téléphone",
|
|
74
|
+
"checkout.address.phoneOptional": "Téléphone (facultatif)",
|
|
75
|
+
"checkout.address.select": "Sélectionnez…",
|
|
63
76
|
"checkout.blocker.cart_loading": "Chargement de votre panier…",
|
|
64
77
|
"checkout.blocker.empty_cart": "Votre panier est vide.",
|
|
65
78
|
"checkout.blocker.billing_incomplete": "Renseignez les champs de contact obligatoires.",
|
|
@@ -60,6 +60,19 @@ export default {
|
|
|
60
60
|
"checkout.hint.none_available": "申し訳ありませんが、その住所への配送はまだ承っておりません。",
|
|
61
61
|
"checkout.hint.syncing": "配送方法を更新しています…",
|
|
62
62
|
"checkout.hint.payment_none": "現在ご購入手続きをご利用いただけません — 有効なお支払い方法がありません。",
|
|
63
|
+
"checkout.address.first_name": "名",
|
|
64
|
+
"checkout.address.last_name": "姓",
|
|
65
|
+
"checkout.address.email": "メールアドレス",
|
|
66
|
+
"checkout.address.company": "会社名",
|
|
67
|
+
"checkout.address.address_1": "住所",
|
|
68
|
+
"checkout.address.address_2": "建物名・部屋番号(任意)",
|
|
69
|
+
"checkout.address.country": "国",
|
|
70
|
+
"checkout.address.city": "市区町村",
|
|
71
|
+
"checkout.address.state": "都道府県 / 地域",
|
|
72
|
+
"checkout.address.postcode": "郵便番号",
|
|
73
|
+
"checkout.address.phone": "電話番号",
|
|
74
|
+
"checkout.address.phoneOptional": "電話番号(任意)",
|
|
75
|
+
"checkout.address.select": "選択してください",
|
|
63
76
|
"checkout.blocker.cart_loading": "カートを読み込んでいます…",
|
|
64
77
|
"checkout.blocker.empty_cart": "カートは空です。",
|
|
65
78
|
"checkout.blocker.billing_incomplete": "必須の連絡先項目をご入力ください。",
|
|
@@ -60,6 +60,19 @@ export default {
|
|
|
60
60
|
"checkout.hint.none_available": "Ainda não entregamos nesse endereço.",
|
|
61
61
|
"checkout.hint.syncing": "Atualizando opções de entrega…",
|
|
62
62
|
"checkout.hint.payment_none": "A finalização está temporariamente indisponível — nenhuma forma de pagamento habilitada.",
|
|
63
|
+
"checkout.address.first_name": "Nome",
|
|
64
|
+
"checkout.address.last_name": "Sobrenome",
|
|
65
|
+
"checkout.address.email": "E-mail",
|
|
66
|
+
"checkout.address.company": "Empresa",
|
|
67
|
+
"checkout.address.address_1": "Endereço",
|
|
68
|
+
"checkout.address.address_2": "Complemento (opcional)",
|
|
69
|
+
"checkout.address.country": "País",
|
|
70
|
+
"checkout.address.city": "Cidade",
|
|
71
|
+
"checkout.address.state": "Estado / Região",
|
|
72
|
+
"checkout.address.postcode": "Código postal",
|
|
73
|
+
"checkout.address.phone": "Telefone",
|
|
74
|
+
"checkout.address.phoneOptional": "Telefone (opcional)",
|
|
75
|
+
"checkout.address.select": "Selecione…",
|
|
63
76
|
"checkout.blocker.cart_loading": "Carregando seu carrinho…",
|
|
64
77
|
"checkout.blocker.empty_cart": "Seu carrinho está vazio.",
|
|
65
78
|
"checkout.blocker.billing_incomplete": "Preencha os campos de contato obrigatórios.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
--_radius: var(--sfui-radius, var(--radius, 0.75rem));
|
|
28
28
|
--_radius-control: var(--sfui-radius-control, calc(var(--_radius) * 0.6));
|
|
29
29
|
--_font-heading: var(--sfui-font-heading, inherit);
|
|
30
|
+
--_font-label: var(--sfui-font-label, inherit);
|
|
30
31
|
|
|
31
32
|
background: transparent;
|
|
32
33
|
color: var(--_on-surface);
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
|
|
48
49
|
.sfui-label {
|
|
49
50
|
display: block;
|
|
51
|
+
font-family: var(--_font-label);
|
|
50
52
|
font-size: 0.6875rem;
|
|
51
53
|
font-weight: 600;
|
|
52
54
|
letter-spacing: 0.08em;
|
|
@@ -379,6 +381,7 @@
|
|
|
379
381
|
}
|
|
380
382
|
.sfui-address [data-part="label"] {
|
|
381
383
|
display: block;
|
|
384
|
+
font-family: var(--_font-label);
|
|
382
385
|
font-size: 0.8125rem;
|
|
383
386
|
font-weight: 500;
|
|
384
387
|
margin-block-end: 0.375rem;
|
|
@@ -400,8 +403,9 @@
|
|
|
400
403
|
.sfui-choice[data-selected="true"] { border-color: var(--_accent); box-shadow: inset 0 0 0 1px var(--_accent); }
|
|
401
404
|
.sfui-choice input { inline-size: 1rem; block-size: 1rem; min-height: 0; margin: 0; accent-color: var(--_accent); flex-shrink: 0; }
|
|
402
405
|
.sfui-choice-main { flex: 1; min-inline-size: 0; }
|
|
403
|
-
|
|
404
|
-
.sfui-choice-
|
|
406
|
+
/* block, not inline: a gateway's description must land under its title */
|
|
407
|
+
.sfui-choice-title { display: block; font-size: 0.9375rem; font-weight: 500; }
|
|
408
|
+
.sfui-choice-desc { display: block; font-size: 0.8125rem; color: var(--_on-subtle); margin-block-start: 0.125rem; }
|
|
405
409
|
.sfui-choice-cost { font-size: 0.9375rem; font-weight: 600; white-space: nowrap; }
|
|
406
410
|
.sfui-chosen-single {
|
|
407
411
|
display: flex;
|
|
@@ -34,13 +34,15 @@ const money = (fn, n) => (typeof fn === "function" ? fn(n) : String(n ?? ""));
|
|
|
34
34
|
*
|
|
35
35
|
* @param {object} rowOrView a `list-products`/`get-product` product, or a
|
|
36
36
|
* `resolveSelection` view
|
|
37
|
-
* @param {{formatMoney?: (n: number) => string}} [opts]
|
|
37
|
+
* @param {{formatMoney?: (n: number) => string, fromLabel?: string}} [opts]
|
|
38
|
+
* `fromLabel` is the from-price prefix — "From" by default, so a store in
|
|
39
|
+
* another language MUST pass its own word or ship one English word per card.
|
|
38
40
|
* @returns {{label: string, compareAtLabel: string|null, onSale: boolean,
|
|
39
41
|
* isFrom: boolean, isRange: boolean, min: number|null, max: number|null}}
|
|
40
42
|
* `label` is what to render as the price; `compareAtLabel` is the
|
|
41
43
|
* struck-through original, or null when the product is not on sale.
|
|
42
44
|
*/
|
|
43
|
-
export function productPrice(rowOrView, { formatMoney } = {}) {
|
|
45
|
+
export function productPrice(rowOrView, { formatMoney, fromLabel = "From" } = {}) {
|
|
44
46
|
const fmt = (n) => money(formatMoney, n);
|
|
45
47
|
const blank = {
|
|
46
48
|
label: "", compareAtLabel: null, onSale: false, isFrom: false,
|
|
@@ -85,7 +87,7 @@ export function productPrice(rowOrView, { formatMoney } = {}) {
|
|
|
85
87
|
const price = row.price ?? null;
|
|
86
88
|
const onSale = Boolean(row.on_sale);
|
|
87
89
|
return {
|
|
88
|
-
label: price == null ? "" : isFrom ?
|
|
90
|
+
label: price == null ? "" : isFrom ? `${fromLabel} ${fmt(price)}` : fmt(price),
|
|
89
91
|
compareAtLabel: onSale && row.regular_price != null ? fmt(row.regular_price) : null,
|
|
90
92
|
onSale,
|
|
91
93
|
isFrom,
|