Skip to content

Commit 16a68e9

Browse files
Tharaaechanceaclark
authored andcommitted
feat: Add out-of-stock/backorder message to product lists (#2739)
1 parent dd1f52d commit 16a68e9

12 files changed

Lines changed: 267 additions & 42 deletions

File tree

.changeset/tricky-plants-teach.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Add out-of-stock / backorder message to product cards on PLPs based on store settings:
6+
- Add out of stock message if the product is out of stock and stock is set to display it.
7+
- Add the backorder message if the product has no on-hand stock and is available for backorder and the store/product is set to display the backorder message
8+
9+
## Migration
10+
11+
### Option 1: Automatic Migration (Recommended)
12+
For existing Catalyst stores, the simplest way to get the newly added feature is to rebase the existing code with the new release code. The files that will be updated are listed below.
13+
14+
### Option 2: Manual Migration
15+
If you prefer not to rebase or have made customizations that prevent rebasing, follow these manual steps:
16+
17+
#### Step 1: Update GraphQL Fragment
18+
Add the inventory fields to your product card fragment in `core/components/product-card/fragment.ts` under `Product`:
19+
```graphql
20+
inventory {
21+
hasVariantInventory
22+
isInStock
23+
aggregated {
24+
availableForBackorder
25+
unlimitedBackorder
26+
availableOnHand
27+
}
28+
}
29+
variants(first: 1) {
30+
edges {
31+
node {
32+
entityId
33+
sku
34+
inventory {
35+
byLocation {
36+
edges {
37+
node {
38+
locationEntityId
39+
backorderMessage
40+
}
41+
}
42+
}
43+
}
44+
}
45+
}
46+
}
47+
```
48+
49+
#### Step 2: Update Product interface in Product Card component
50+
Update the `Product` interface in `core/vibes/soul/primitives/product-card/index.tsx` adding the following field to it:
51+
52+
`inventoryMessage?: string;`
53+
54+
#### Step 3: Update Data Transformer
55+
Modify `core/data-transformers/product-card-transformer.ts` to include inventory message in the transformed data. You can simply copy the whole file from this release as it does not have UI breaking changes.
56+
57+
#### Step 4: Update Product Card Layout
58+
Update `core/vibes/soul/primitives/product-card/index.tsx` layout to display the new `inventoryMessage` product field.
59+
60+
#### Step 5: Update Page Data GraphQL queries
61+
Add inventory settings queries to the pages data. Add the following query to the main GQL query under `site.settings`:
62+
```
63+
inventory {
64+
defaultOutOfStockMessage
65+
showOutOfStockMessage
66+
showBackorderMessage
67+
}
68+
```
69+
to the following page data files:
70+
- `core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts`
71+
- `core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts`
72+
- `core/app/[locale]/(default)/(faceted)/search/page-data.ts`
73+
- `core/app/[locale]/(default)/page-data.ts`
74+
75+
#### Step 6: Update Page Components
76+
Update the corresponding page components to use the `productCardTransformer` method (if not already using it) to get the product card, and pass inventory data to those product cards based on the store inventory settings. Use the following code while retrieving the product lists:
77+
```
78+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
79+
data.site.settings?.inventory ?? {};
80+
81+
return productCardTransformer(
82+
featuredProducts,
83+
format,
84+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
85+
showBackorderMessage,
86+
);
87+
```
88+
in the following files:
89+
- `core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx`
90+
- `core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx`
91+
- `core/app/[locale]/(default)/(faceted)/search/page.tsx`
92+
- `core/app/[locale]/(default)/page.tsx`
93+
94+
### Files Modified in This Change
95+
- `core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts`
96+
- `core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx`
97+
- `core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts`
98+
- `core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx`
99+
- `core/app/[locale]/(default)/(faceted)/search/page-data.ts`
100+
- `core/app/[locale]/(default)/(faceted)/search/page.tsx`
101+
- `core/app/[locale]/(default)/page-data.ts`
102+
- `core/app/[locale]/(default)/page.tsx`
103+
- `core/components/product-card/fragment.ts`
104+
- `core/data-transformers/product-card-transformer.ts`
105+
- `core/vibes/soul/primitives/product-card/index.tsx`

core/app/[locale]/(default)/(faceted)/brand/[slug]/page-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const BrandPageQuery = graphql(`
1616
}
1717
}
1818
settings {
19+
inventory {
20+
defaultOutOfStockMessage
21+
showOutOfStockMessage
22+
showBackorderMessage
23+
}
1924
storefront {
2025
catalog {
2126
productComparisonsEnabled

core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getFilterParsers } from '@/vibes/soul/sections/products-list-section/fi
1111
import { getSessionCustomerAccessToken } from '~/auth';
1212
import { facetsTransformer } from '~/data-transformers/facets-transformer';
1313
import { pageInfoTransformer } from '~/data-transformers/page-info-transformer';
14-
import { pricesTransformer } from '~/data-transformers/prices-transformer';
14+
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
1515
import { getPreferredCurrencyCode } from '~/lib/currency';
1616

1717
import { MAX_COMPARE_LIMIT } from '../../../compare/page-data';
@@ -132,16 +132,15 @@ export default async function Brand(props: Props) {
132132
const search = await streamableFacetedSearch;
133133
const products = search.products.items;
134134

135-
return products.map((product) => ({
136-
id: product.entityId.toString(),
137-
title: product.name,
138-
href: product.path,
139-
image: product.defaultImage
140-
? { src: product.defaultImage.url, alt: product.defaultImage.altText }
141-
: undefined,
142-
price: pricesTransformer(product.prices, format),
143-
subtitle: product.brand?.name ?? undefined,
144-
}));
135+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
136+
settings?.inventory ?? {};
137+
138+
return productCardTransformer(
139+
products,
140+
format,
141+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
142+
showBackorderMessage,
143+
);
145144
});
146145

147146
const streamableTotalCount = Streamable.from(async () => {

core/app/[locale]/(default)/(faceted)/category/[slug]/page-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ const CategoryPageQuery = graphql(
3535
}
3636
}
3737
settings {
38+
inventory {
39+
defaultOutOfStockMessage
40+
showOutOfStockMessage
41+
showBackorderMessage
42+
}
3843
storefront {
3944
catalog {
4045
productComparisonsEnabled

core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { getFilterParsers } from '@/vibes/soul/sections/products-list-section/fi
1212
import { getSessionCustomerAccessToken } from '~/auth';
1313
import { facetsTransformer } from '~/data-transformers/facets-transformer';
1414
import { pageInfoTransformer } from '~/data-transformers/page-info-transformer';
15-
import { pricesTransformer } from '~/data-transformers/prices-transformer';
15+
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
1616
import { getPreferredCurrencyCode } from '~/lib/currency';
1717

1818
import { MAX_COMPARE_LIMIT } from '../../../compare/page-data';
@@ -145,16 +145,15 @@ export default async function Category(props: Props) {
145145
const search = await streamableFacetedSearch;
146146
const products = search.products.items;
147147

148-
return products.map((product) => ({
149-
id: product.entityId.toString(),
150-
title: product.name,
151-
href: product.path,
152-
image: product.defaultImage
153-
? { src: product.defaultImage.url, alt: product.defaultImage.altText }
154-
: undefined,
155-
price: pricesTransformer(product.prices, format),
156-
subtitle: product.brand?.name ?? undefined,
157-
}));
148+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
149+
settings?.inventory ?? {};
150+
151+
return productCardTransformer(
152+
products,
153+
format,
154+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
155+
showBackorderMessage,
156+
);
158157
});
159158

160159
const streamableTotalCount = Streamable.from(async () => {

core/app/[locale]/(default)/(faceted)/search/page-data.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ const SearchPageQuery = graphql(`
88
query SearchPageQuery {
99
site {
1010
settings {
11+
inventory {
12+
defaultOutOfStockMessage
13+
showOutOfStockMessage
14+
showBackorderMessage
15+
}
1116
storefront {
1217
catalog {
1318
productComparisonsEnabled

core/app/[locale]/(default)/(faceted)/search/page.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getFilterParsers } from '@/vibes/soul/sections/products-list-section/fi
1010
import { getSessionCustomerAccessToken } from '~/auth';
1111
import { facetsTransformer } from '~/data-transformers/facets-transformer';
1212
import { pageInfoTransformer } from '~/data-transformers/page-info-transformer';
13-
import { pricesTransformer } from '~/data-transformers/prices-transformer';
13+
import { productCardTransformer } from '~/data-transformers/product-card-transformer';
1414
import { getPreferredCurrencyCode } from '~/lib/currency';
1515

1616
import { MAX_COMPARE_LIMIT } from '../../compare/page-data';
@@ -117,16 +117,15 @@ export default async function Search(props: Props) {
117117
const search = await streamableFacetedSearch;
118118
const products = search.products.items;
119119

120-
return products.map((product) => ({
121-
id: product.entityId.toString(),
122-
title: product.name,
123-
href: product.path,
124-
image: product.defaultImage
125-
? { src: product.defaultImage.url, alt: product.defaultImage.altText }
126-
: undefined,
127-
price: pricesTransformer(product.prices, format),
128-
subtitle: product.brand?.name ?? undefined,
129-
}));
120+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
121+
settings?.inventory ?? {};
122+
123+
return productCardTransformer(
124+
products,
125+
format,
126+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
127+
showBackorderMessage,
128+
);
130129
});
131130

132131
const streamableTitle = Streamable.from(async () => {

core/app/[locale]/(default)/page-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ const HomePageQuery = graphql(
6161
}
6262
}
6363
}
64+
settings {
65+
inventory {
66+
defaultOutOfStockMessage
67+
showOutOfStockMessage
68+
showBackorderMessage
69+
}
70+
}
6471
}
6572
}
6673
`,

core/app/[locale]/(default)/page.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,31 @@ export default async function Home({ params }: Props) {
3636

3737
const featuredProducts = removeEdgesAndNodes(data.site.featuredProducts);
3838

39-
return productCardTransformer(featuredProducts, format);
39+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
40+
data.site.settings?.inventory ?? {};
41+
42+
return productCardTransformer(
43+
featuredProducts,
44+
format,
45+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
46+
showBackorderMessage,
47+
);
4048
});
4149

4250
const streamableNewestProducts = Streamable.from(async () => {
4351
const data = await streamablePageData;
4452

4553
const newestProducts = removeEdgesAndNodes(data.site.newestProducts);
4654

47-
return productCardTransformer(newestProducts, format);
55+
const { defaultOutOfStockMessage, showOutOfStockMessage, showBackorderMessage } =
56+
data.site.settings?.inventory ?? {};
57+
58+
return productCardTransformer(
59+
newestProducts,
60+
format,
61+
showOutOfStockMessage ? defaultOutOfStockMessage : undefined,
62+
showBackorderMessage,
63+
);
4864
});
4965

5066
return (

core/components/product-card/fragment.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,37 @@ export const ProductCardFragment = graphql(
1515
name
1616
path
1717
}
18+
inventory {
19+
hasVariantInventory
20+
isInStock
21+
aggregated {
22+
availableForBackorder
23+
unlimitedBackorder
24+
availableOnHand
25+
}
26+
}
1827
reviewSummary {
1928
numberOfReviews
2029
averageRating
2130
}
31+
variants(first: 1) {
32+
edges {
33+
node {
34+
entityId
35+
sku
36+
inventory {
37+
byLocation {
38+
edges {
39+
node {
40+
locationEntityId
41+
backorderMessage
42+
}
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
2249
...PricingFragment
2350
}
2451
`,

0 commit comments

Comments
 (0)