Skip to content

Commit 53474a0

Browse files
mirekmlkostrowski
andauthored
Warn user about new stock events in the webhook event picker (#6549)
Co-authored-by: Lukasz Ostrowski <lukasz.ostrowski@saleor.io>
1 parent 268465b commit 53474a0

7 files changed

Lines changed: 207 additions & 33 deletions

File tree

.changeset/bright-otters-dance.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Webhook event picker: warn admins about Saleor 3.23 channel-scoped stock events.
6+
7+
The four `PRODUCT_VARIANT_BACK_IN_STOCK_*` / `PRODUCT_VARIANT_OUT_OF_STOCK_*` events introduced in Saleor 3.23 fire only when the shop has the legacy shipping-zone stock-availability setting disabled. They were already exposed in the picker (auto-derived from the schema), but admins on shops still in legacy mode could subscribe with no visual cue and silently never receive deliveries. Each of those four events now shows an advisory "Direct stock mode only" badge with a tooltip linking to the site-settings page where the flag is configured. Adds a regression test pinning the four events to the `PRODUCT_VARIANT` group, and a comment in `ExcludedDocumentKeys` documenting that the dry-run feature already covers them transitively via prefix matching.

locale/defaultMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,6 +2333,10 @@
23332333
"context": "delete channel",
23342334
"string": "Select channel that you wish to move existing orders to."
23352335
},
2336+
"BSGid2": {
2337+
"context": "Advisory badge shown next to a webhook event in the picker when the event only fires while the shop has the direct (non-legacy) warehouse-channel stock-availability mode enabled.",
2338+
"string": "Direct stock mode only"
2339+
},
23362340
"BUKMzM": {
23372341
"string": "Variant removed"
23382342
},
@@ -2392,6 +2396,10 @@
23922396
"context": "header",
23932397
"string": "Translation Collection \"{collectionName}\" - {languageCode}"
23942398
},
2399+
"BrRGIJ": {
2400+
"context": "Tooltip body for the direct-stock-mode-only badge in the webhook event picker. Quotes the exact label of the toggle on the site-settings page so the admin recognizes it when they click through.",
2401+
"string": "Fires only while \"Use legacy shipping zone stock availability\" is disabled in site settings. Otherwise the subscription produces no deliveries."
2402+
},
23952403
"BtsJ+e": {
23962404
"context": "empty state message when no login method is available",
23972405
"string": "Password login is disabled. Contact your administrator to configure an external authentication method or enable password login."
@@ -10710,6 +10718,10 @@
1071010718
"vfG+nh": {
1071110719
"string": "Confirm Password"
1071210720
},
10721+
"vh9a5S": {
10722+
"context": "Tooltip CTA on the direct-stock-mode-only badge in the webhook event picker. Links to the page where the legacy stock-availability flag can be toggled.",
10723+
"string": "Open shop settings"
10724+
},
1071310725
"viFkCw": {
1071410726
"context": "site settings section name",
1071510727
"string": "Site Settings"

src/components/DryRunItemsList/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@ export const DocumentMap: Record<string, Document> = {
219219

220220
// Documents which require parent object or can't be handled ATM
221221
//
222+
// Note: matching is done via `event.startsWith(object)` in
223+
// `src/components/DryRun/utils.ts`, so prefix entries also cover their
224+
// channel-scoped descendants — e.g. `PRODUCT_VARIANT_BACK_IN_STOCK` covers the
225+
// Saleor 3.23+ events `PRODUCT_VARIANT_BACK_IN_STOCK_FOR_CLICK_AND_COLLECT`
226+
// and `PRODUCT_VARIANT_BACK_IN_STOCK_IN_CHANNEL`, and `PRODUCT_VARIANT_OUT_OF_STOCK`
227+
// covers the corresponding `_FOR_CLICK_AND_COLLECT` / `_IN_CHANNEL` variants.
228+
// If you ever switch the matcher to exact-equality, expand these prefixes to
229+
// list the concrete event names individually.
222230
export const ExcludedDocumentKeys = [
223231
// USER ID REQUIRED
224232
"ADDRESS",

src/extensions/components/WebhookDetailsPage/components/WebhookEvents/WebhookEvents.tsx

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { DashboardCard } from "@dashboard/components/Card";
22
import Grid from "@dashboard/components/Grid";
33
import Hr from "@dashboard/components/Hr";
4+
import { Link } from "@dashboard/components/Link";
45
import { type WebhookEventTypeAsyncEnum, type WebhookEventTypeSyncEnum } from "@dashboard/graphql";
56
import { type ChangeEvent } from "@dashboard/hooks/useForm";
67
import { capitalize } from "@dashboard/misc";
8+
import { siteSettingsUrl } from "@dashboard/siteSettings/urls";
79
import {
810
List,
911
ListBody,
@@ -12,13 +14,13 @@ import {
1214
ListItemCell,
1315
useListWidths,
1416
} from "@saleor/macaw-ui";
15-
import { Box, Checkbox, Chip, Switch, Text } from "@saleor/macaw-ui-next";
17+
import { Box, Checkbox, Chip, Switch, Text, Tooltip } from "@saleor/macaw-ui-next";
1618
import { type Dispatch, type SetStateAction, useState } from "react";
1719
import { FormattedMessage, useIntl } from "react-intl";
1820

1921
import { messages } from "./messages";
2022
import { useStyles } from "./styles";
21-
import { EventTypes, getEventName } from "./utils";
23+
import { EventTypes, getEventName, isDirectStockModeOnlyEvent } from "./utils";
2224

2325
interface WebhookEventsProps {
2426
data: {
@@ -147,36 +149,75 @@ export const WebhookEvents = ({
147149
<ListBody className={classes.listBody}>
148150
{object &&
149151
EventTypes[tab][object] &&
150-
EventTypes[tab][object].map((event, idx) => (
151-
<ListItem className={classes.eventListItem} key={event}>
152-
<ListItemCell className={classes.eventListItemCell}>
153-
<Checkbox
154-
data-test-id="events-checkbox"
155-
name={`${tab}Events`}
156-
checked={(data[`${tab}Events`] as WebhookEventTypeSyncEnum[]).includes(
157-
getEventName(object, event),
158-
)}
159-
value={getEventName(object, event)}
160-
onCheckedChange={checked =>
161-
handleEventChange({
162-
target: {
163-
name: `${tab}Events`,
164-
value: getEventName(object, event),
165-
// @ts-expect-error incorrect useForm types - cannot set required checked property
166-
checked,
167-
},
168-
})
169-
}
170-
id={`event-checkbox-${idx}`}
171-
paddingX={1.5}
172-
paddingY={3.5}
173-
fontWeight="bold"
174-
>
175-
{capitalize(event.toLowerCase().replaceAll("_", " "))}
176-
</Checkbox>
177-
</ListItemCell>
178-
</ListItem>
179-
))}
152+
EventTypes[tab][object].map((event, idx) => {
153+
const eventName = getEventName(object, event);
154+
const showDirectStockModeBadge = isDirectStockModeOnlyEvent(eventName);
155+
156+
return (
157+
<ListItem className={classes.eventListItem} key={event}>
158+
<ListItemCell className={classes.eventListItemCell}>
159+
<Box display="flex" alignItems="center" gap={2}>
160+
<Checkbox
161+
data-test-id="events-checkbox"
162+
name={`${tab}Events`}
163+
checked={(
164+
data[`${tab}Events`] as WebhookEventTypeSyncEnum[]
165+
).includes(eventName)}
166+
value={eventName}
167+
onCheckedChange={checked =>
168+
handleEventChange({
169+
target: {
170+
name: `${tab}Events`,
171+
value: eventName,
172+
// @ts-expect-error incorrect useForm types - cannot set required checked property
173+
checked,
174+
},
175+
})
176+
}
177+
id={`event-checkbox-${idx}`}
178+
paddingX={1.5}
179+
paddingY={3.5}
180+
fontWeight="bold"
181+
>
182+
{capitalize(event.toLowerCase().replaceAll("_", " "))}
183+
</Checkbox>
184+
{showDirectStockModeBadge && (
185+
<Tooltip>
186+
<Tooltip.Trigger>
187+
<Chip
188+
data-test-id="direct-stock-mode-badge"
189+
backgroundColor="warning1"
190+
borderColor="warning1"
191+
color="warning1"
192+
size="small"
193+
__cursor="help"
194+
>
195+
{intl.formatMessage(messages.directStockModeBadge)}
196+
</Chip>
197+
</Tooltip.Trigger>
198+
<Tooltip.Content side="top">
199+
<Tooltip.Arrow />
200+
<Box
201+
__maxWidth="320px"
202+
display="flex"
203+
flexDirection="column"
204+
gap={2}
205+
>
206+
<Text size={2}>
207+
<FormattedMessage {...messages.directStockModeTooltipBody} />
208+
</Text>
209+
<Link href={siteSettingsUrl()} underline>
210+
<FormattedMessage {...messages.directStockModeTooltipLink} />
211+
</Link>
212+
</Box>
213+
</Tooltip.Content>
214+
</Tooltip>
215+
)}
216+
</Box>
217+
</ListItemCell>
218+
</ListItem>
219+
);
220+
})}
180221
</ListBody>
181222
</List>
182223
</div>

src/extensions/components/WebhookDetailsPage/components/WebhookEvents/messages.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,23 @@ export const messages = defineMessages({
3737
defaultMessage: "Webhook events",
3838
description: "Webhook events header",
3939
},
40+
directStockModeBadge: {
41+
id: "BSGid2",
42+
defaultMessage: "Direct stock mode only",
43+
description:
44+
"Advisory badge shown next to a webhook event in the picker when the event only fires while the shop has the direct (non-legacy) warehouse-channel stock-availability mode enabled.",
45+
},
46+
directStockModeTooltipBody: {
47+
id: "BrRGIJ",
48+
defaultMessage:
49+
'Fires only while "Use legacy shipping zone stock availability" is disabled in site settings. Otherwise the subscription produces no deliveries.',
50+
description:
51+
"Tooltip body for the direct-stock-mode-only badge in the webhook event picker. Quotes the exact label of the toggle on the site-settings page so the admin recognizes it when they click through.",
52+
},
53+
directStockModeTooltipLink: {
54+
id: "vh9a5S",
55+
defaultMessage: "Open shop settings",
56+
description:
57+
"Tooltip CTA on the direct-stock-mode-only badge in the webhook event picker. Links to the page where the legacy stock-availability flag can be toggled.",
58+
},
4059
});

src/extensions/components/WebhookDetailsPage/components/WebhookEvents/utils.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { getWebhookTypes } from "./utils";
1+
import { WebhookEventTypeAsyncEnum } from "@dashboard/graphql";
2+
3+
import {
4+
DIRECT_STOCK_MODE_ONLY_EVENTS,
5+
getWebhookTypes,
6+
isDirectStockModeOnlyEvent,
7+
} from "./utils";
28

39
const TestKeys = [
410
"DRAFT_ORDER_CREATED",
@@ -17,4 +23,63 @@ describe("getWebhookTypes", () => {
1723
expect(TestWebhookTypes.PRODUCT).toEqual(["CREATED", "UPDATED"]);
1824
expect(TestWebhookTypes.PRODUCT_VARIANT).toEqual(["UPDATED"]);
1925
});
26+
27+
// Regression guard for Saleor 3.23+ channel-scoped variant stock events.
28+
// These four events must remain grouped under PRODUCT_VARIANT in the picker
29+
// so admins can find them. If the schema removes any of them, or the keyword
30+
// grouping logic changes, this test fails loudly instead of silently dropping
31+
// the events from the UI.
32+
it("groups the Saleor 3.23 channel-scoped stock events under PRODUCT_VARIANT", () => {
33+
const types = getWebhookTypes(Object.keys(WebhookEventTypeAsyncEnum));
34+
35+
expect(types.PRODUCT_VARIANT).toEqual(
36+
expect.arrayContaining([
37+
"BACK_IN_STOCK_FOR_CLICK_AND_COLLECT",
38+
"BACK_IN_STOCK_IN_CHANNEL",
39+
"OUT_OF_STOCK_FOR_CLICK_AND_COLLECT",
40+
"OUT_OF_STOCK_IN_CHANNEL",
41+
]),
42+
);
43+
});
44+
});
45+
46+
describe("isDirectStockModeOnlyEvent", () => {
47+
it("returns true for each of the four channel-scoped stock events", () => {
48+
expect(
49+
isDirectStockModeOnlyEvent(
50+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_FOR_CLICK_AND_COLLECT,
51+
),
52+
).toBe(true);
53+
expect(
54+
isDirectStockModeOnlyEvent(
55+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_IN_CHANNEL,
56+
),
57+
).toBe(true);
58+
expect(
59+
isDirectStockModeOnlyEvent(
60+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_FOR_CLICK_AND_COLLECT,
61+
),
62+
).toBe(true);
63+
expect(
64+
isDirectStockModeOnlyEvent(WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_IN_CHANNEL),
65+
).toBe(true);
66+
});
67+
68+
it("returns false for the legacy (non-channel-scoped) stock events", () => {
69+
expect(
70+
isDirectStockModeOnlyEvent(WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK),
71+
).toBe(false);
72+
expect(isDirectStockModeOnlyEvent(WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK)).toBe(
73+
false,
74+
);
75+
});
76+
77+
it("returns false for unrelated events and arbitrary strings", () => {
78+
expect(isDirectStockModeOnlyEvent(WebhookEventTypeAsyncEnum.PRODUCT_CREATED)).toBe(false);
79+
expect(isDirectStockModeOnlyEvent("NOT_A_REAL_EVENT")).toBe(false);
80+
});
81+
82+
it("exports a set whose size matches the documented count", () => {
83+
expect(DIRECT_STOCK_MODE_ONLY_EVENTS.size).toBe(4);
84+
});
2085
});

src/extensions/components/WebhookDetailsPage/components/WebhookEvents/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ import { WebhookEventTypeAsyncEnum, WebhookEventTypeSyncEnum } from "@dashboard/
22

33
type Actions = string[];
44

5+
/**
6+
* Saleor 3.23 introduced four channel-scoped variant stock-availability events
7+
* that fire only when `Shop.useLegacyShippingZoneStockAvailability` is `false`
8+
* (i.e. the new direct warehouse-channel stock-availability mode is enabled).
9+
* Subscribing to them on a shop still in legacy mode is a silent footgun — the
10+
* webhook is saved, but no deliveries are ever produced.
11+
*
12+
* The picker surfaces a small advisory badge next to each of these events so
13+
* admins see the prerequisite at the moment of subscription, regardless of
14+
* the shop's current mode. Keep this list in sync with the schema if Saleor
15+
* adds further events with the same precondition.
16+
*/
17+
export const DIRECT_STOCK_MODE_ONLY_EVENTS: ReadonlySet<WebhookEventTypeAsyncEnum> = new Set([
18+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_FOR_CLICK_AND_COLLECT,
19+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_IN_CHANNEL,
20+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_FOR_CLICK_AND_COLLECT,
21+
WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_IN_CHANNEL,
22+
]);
23+
24+
export const isDirectStockModeOnlyEvent = (eventName: string): boolean =>
25+
DIRECT_STOCK_MODE_ONLY_EVENTS.has(eventName as WebhookEventTypeAsyncEnum);
26+
527
export const getWebhookTypes = (webhookEvents: string[]) => {
628
const multiWords = ["DRAFT_ORDER", "GIFT_CARD", "ANY_EVENTS", "PRODUCT_VARIANT"];
729

0 commit comments

Comments
 (0)