Skip to content

Commit 22077f1

Browse files
authored
fix(admin): handle null sales channel references in product list and detail views (#15009)
## What Fixes a crash in the Admin UI when a product has orphaned sales channel references (i.e., the sales channel was deleted but the product association was not cleaned up). ## Why When a sales channel is deleted, the join table entries linking it to products are not always cleaned up. This results in the API returning `null` entries in the product's `sales_channels` array: ```json { "sales_channels": [null] } ``` The Admin UI attempts to access `sales_channel.name` without null checks, causing: ``` TypeError: Cannot read properties of null (reading 'name') ``` This crashes both the product list page and the product detail page. ## How Added defensive null filtering in two components: 1. **`SalesChannelsCell`** (product list table): Filters out `null`/`undefined` entries from the `salesChannels` array before rendering. Uses a TypeScript type guard to maintain type safety. 2. **`ProductSalesChannelSection`** (product detail page): Filters out `null`/`undefined` entries before mapping sales channels for display. Both components now gracefully handle orphaned references by simply excluding them from the rendered output, showing only valid sales channels. ## Testing - Product with all valid sales channels → renders normally (no change) - Product with `sales_channels: [null]` → shows placeholder instead of crashing - Product with mix of valid and null channels → shows only valid channels - Product with `sales_channels: []` or `null` → shows placeholder (existing behavior, unchanged) Fixes #14945 --- > [!NOTE] > **Low Risk** > Low risk UI-only change that adds defensive null filtering to prevent runtime crashes when the API returns orphaned `sales_channels` entries; behavior change is limited to hiding invalid entries. > > **Overview** > Prevents Admin UI crashes when products contain orphaned `sales_channels` entries returned as `null`. > > Adds defensive filtering in the product list `SalesChannelsCell` and product detail `ProductSalesChannelSection` so only non-null sales channels are rendered (falling back to existing placeholder/empty states). Includes a patch changeset for `@medusajs/dashboard`. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 75478bf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
1 parent 1a5d2e0 commit 22077f1

File tree

3 files changed

+25
-10
lines changed

3 files changed

+25
-10
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/dashboard": patch
3+
---
4+
5+
fix(admin): handle null sales channel references in product list and detail views

packages/admin/dashboard/src/components/table/table-cells/product/sales-channels-cell/sales-channels-cell.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,45 @@ export const SalesChannelsCell = ({
1313
}: SalesChannelsCellProps) => {
1414
const { t } = useTranslation()
1515

16-
if (!salesChannels || !salesChannels.length) {
16+
// Filter out null/undefined entries that can occur when a sales channel
17+
// is deleted but the product association is not cleaned up
18+
const validChannels = salesChannels?.filter(
19+
(sc): sc is SalesChannelDTO => sc != null
20+
)
21+
22+
if (!validChannels || !validChannels.length) {
1723
return <PlaceholderCell />
1824
}
1925

20-
if (salesChannels.length > 2) {
26+
if (validChannels.length > 2) {
2127
return (
2228
<div className="flex h-full w-full items-center gap-x-1 overflow-hidden">
2329
<span className="truncate">
24-
{salesChannels
30+
{validChannels
2531
.slice(0, 2)
2632
.map((sc) => sc.name)
2733
.join(", ")}
2834
</span>
2935
<Tooltip
3036
content={
3137
<ul>
32-
{salesChannels.slice(2).map((sc) => (
38+
{validChannels.slice(2).map((sc) => (
3339
<li key={sc.id}>{sc.name}</li>
3440
))}
3541
</ul>
3642
}
3743
>
3844
<span className="text-xs">
3945
{t("general.plusCountMore", {
40-
count: salesChannels.length - 2,
46+
count: validChannels.length - 2,
4147
})}
4248
</span>
4349
</Tooltip>
4450
</div>
4551
)
4652
}
4753

48-
const channels = salesChannels.map((sc) => sc.name).join(", ")
54+
const channels = validChannels.map((sc) => sc.name).join(", ")
4955

5056
return (
5157
<div className="flex h-full w-full items-center overflow-hidden max-w-[250px]">

packages/admin/dashboard/src/routes/products/product-detail/components/product-sales-channel-section/product-sales-channel-section.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ export const ProductSalesChannelSection = ({
1616
const { count } = useSalesChannels()
1717
const { t } = useTranslation()
1818

19+
// Filter out null/undefined entries that can occur when a sales channel
20+
// is deleted but the product association is not cleaned up
1921
const availableInSalesChannels =
20-
product.sales_channels?.map((sc) => ({
21-
id: sc.id,
22-
name: sc.name,
23-
})) ?? []
22+
product.sales_channels
23+
?.filter((sc) => sc != null)
24+
.map((sc) => ({
25+
id: sc.id,
26+
name: sc.name,
27+
})) ?? []
2428

2529
const firstChannels = availableInSalesChannels.slice(0, 3)
2630
const restChannels = availableInSalesChannels.slice(3)

0 commit comments

Comments
 (0)