diff --git a/.changeset/quick-items-change.md b/.changeset/quick-items-change.md
new file mode 100644
index 00000000000..8a1e2329742
--- /dev/null
+++ b/.changeset/quick-items-change.md
@@ -0,0 +1,6 @@
+---
+"saleor-dashboard": patch
+---
+
+Added a "Stock availability" toggle in Site Settings to control `useLegacyShippingZoneStockAvailability`.
+
diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json
index cc866e49b35..7bb5fac6bdb 100644
--- a/locale/defaultMessages.json
+++ b/locale/defaultMessages.json
@@ -1472,6 +1472,10 @@
"6Y1YDn": {
"string": "Updated extension permissions"
},
+ "6ZnMfQ": {
+ "context": "card header and checkbox label",
+ "string": "Use legacy shipping zone stock availability"
+ },
"6ZubLQ": {
"string": "Manage available refunds reasons"
},
@@ -9710,6 +9714,10 @@
"context": "order history message",
"string": "Invoice was sent to customer by {sentBy}"
},
+ "qeAmFa": {
+ "context": "section description",
+ "string": "When enabled, stock availability is filtered by shipping zones and the destination address (legacy behavior). When disabled, it is determined only by the direct warehouse-channel link. Learn more."
+ },
"qf8OtW": {
"string": "Continue with Saleor Cloud"
},
@@ -10496,6 +10504,10 @@
"v3WWK+": {
"string": "Status is invalid"
},
+ "v5yLSE": {
+ "context": "section title",
+ "string": "Stock availability"
+ },
"v8UngX": {
"string": "Search warehouses..."
},
@@ -10872,6 +10884,10 @@
"context": "dialog header",
"string": "Cancel order #{orderNumber}"
},
+ "wmybDi": {
+ "context": "intro to webhook list",
+ "string": "When disabled, the following channel-scoped stock webhooks become available:"
+ },
"wn3di2": {
"string": "This password is too commonly used"
},
diff --git a/src/fragments/shop.ts b/src/fragments/shop.ts
index 9812ac2baa6..30e9480f0b4 100644
--- a/src/fragments/shop.ts
+++ b/src/fragments/shop.ts
@@ -57,6 +57,7 @@ export const shopFragment = gql`
limitQuantityPerCheckout
enableAccountConfirmationByEmail
useLegacyUpdateWebhookEmission
+ useLegacyShippingZoneStockAvailability
preserveAllAddressFields
passwordLoginMode
}
diff --git a/src/graphql/hooks.generated.ts b/src/graphql/hooks.generated.ts
index 1a21d2350e3..036650f69d1 100644
--- a/src/graphql/hooks.generated.ts
+++ b/src/graphql/hooks.generated.ts
@@ -3407,6 +3407,7 @@ export const ShopFragmentDoc = gql`
limitQuantityPerCheckout
enableAccountConfirmationByEmail
useLegacyUpdateWebhookEmission
+ useLegacyShippingZoneStockAvailability
preserveAllAddressFields
passwordLoginMode
}
diff --git a/src/graphql/types.generated.ts b/src/graphql/types.generated.ts
index 1b8cf0d4f50..aefd34fb311 100644
--- a/src/graphql/types.generated.ts
+++ b/src/graphql/types.generated.ts
@@ -11789,7 +11789,7 @@ export type LimitInfoFragment = { __typename: 'Limits', channels?: number | null
export type ShopLimitFragment = { __typename: 'Shop', limits: { __typename: 'LimitInfo', currentUsage: { __typename: 'Limits', channels?: number | null, orders?: number | null, productVariants?: number | null, staffUsers?: number | null, warehouses?: number | null }, allowedUsage: { __typename: 'Limits', channels?: number | null, orders?: number | null, productVariants?: number | null, staffUsers?: number | null, warehouses?: number | null } } };
-export type ShopFragment = { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } };
+export type ShopFragment = { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, useLegacyShippingZoneStockAvailability: boolean, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } };
export type StaffMemberFragment = { __typename: 'User', id: string, email: string, firstName: string, isActive: boolean, lastName: string };
@@ -13432,7 +13432,7 @@ export type ShopSettingsUpdateMutationVariables = Exact<{
}>;
-export type ShopSettingsUpdateMutation = { __typename: 'Mutation', shopSettingsUpdate: { __typename: 'ShopSettingsUpdate', errors: Array<{ __typename: 'ShopError', code: ShopErrorCode, field: string | null, message: string | null }>, shop: { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } } | null } | null, shopAddressUpdate: { __typename: 'ShopAddressUpdate', errors: Array<{ __typename: 'ShopError', code: ShopErrorCode, field: string | null, message: string | null }>, shop: { __typename: 'Shop', companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null } | null } | null };
+export type ShopSettingsUpdateMutation = { __typename: 'Mutation', shopSettingsUpdate: { __typename: 'ShopSettingsUpdate', errors: Array<{ __typename: 'ShopError', code: ShopErrorCode, field: string | null, message: string | null }>, shop: { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, useLegacyShippingZoneStockAvailability: boolean, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } } | null } | null, shopAddressUpdate: { __typename: 'ShopAddressUpdate', errors: Array<{ __typename: 'ShopError', code: ShopErrorCode, field: string | null, message: string | null }>, shop: { __typename: 'Shop', companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null } | null } | null };
export type RefundSettingsUpdateMutationVariables = Exact<{
refundSettingsInput: RefundSettingsUpdateInput;
@@ -13449,7 +13449,7 @@ export type RefundReasonReferenceClearMutation = { __typename: 'Mutation', refun
export type SiteSettingsQueryVariables = Exact<{ [key: string]: never; }>;
-export type SiteSettingsQuery = { __typename: 'Query', shop: { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } } };
+export type SiteSettingsQuery = { __typename: 'Query', shop: { __typename: 'Shop', customerSetPasswordUrl: string | null, defaultMailSenderAddress: string | null, defaultMailSenderName: string | null, description: string | null, name: string, reserveStockDurationAnonymousUser: number | null, reserveStockDurationAuthenticatedUser: number | null, limitQuantityPerCheckout: number | null, enableAccountConfirmationByEmail: boolean | null, useLegacyUpdateWebhookEmission: boolean | null, useLegacyShippingZoneStockAvailability: boolean, preserveAllAddressFields: boolean, passwordLoginMode: PasswordLoginModeEnum, companyAddress: { __typename: 'Address', city: string, cityArea: string, companyName: string, countryArea: string, firstName: string, id: string, lastName: string, phone: string | null, postalCode: string, streetAddress1: string, streetAddress2: string, country: { __typename: 'CountryDisplay', code: string, country: string } } | null, countries: Array<{ __typename: 'CountryDisplay', code: string, country: string }>, domain: { __typename: 'Domain', host: string } } };
export type StaffMemberAddMutationVariables = Exact<{
input: StaffCreateInput;
diff --git a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx
index d3e363ce1bd..6efc48fca67 100644
--- a/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx
+++ b/src/siteSettings/components/SiteSettingsPage/SiteSettingsPage.tsx
@@ -6,13 +6,16 @@ import CompanyAddressInput from "@dashboard/components/CompanyAddressInput";
import { type ConfirmButtonTransitionState } from "@dashboard/components/ConfirmButton";
import Form from "@dashboard/components/Form";
import { DetailPageLayout } from "@dashboard/components/Layouts";
+import { Link } from "@dashboard/components/Link";
import PageSectionHeader from "@dashboard/components/PageSectionHeader";
import { Savebar } from "@dashboard/components/Savebar";
+import VerticalSpacer from "@dashboard/components/VerticalSpacer";
import { configurationMenuUrl } from "@dashboard/configuration/urls";
import {
type PasswordLoginModeEnum,
type ShopErrorFragment,
type SiteSettingsQuery,
+ WebhookEventTypeAsyncEnum,
} from "@dashboard/graphql";
import useAddressValidation from "@dashboard/hooks/useAddressValidation";
import { type SubmitPromise } from "@dashboard/hooks/useForm";
@@ -22,12 +25,22 @@ import { commonMessages } from "@dashboard/intl";
import createSingleAutocompleteSelectHandler from "@dashboard/utils/handlers/singleAutocompleteSelectChangeHandler";
import { mapCountriesToChoices } from "@dashboard/utils/maps";
import { Box, Checkbox, Divider, Text } from "@saleor/macaw-ui-next";
-import { useIntl } from "react-intl";
+import { FormattedMessage, useIntl } from "react-intl";
import SiteCheckoutSettingsCard from "../SiteCheckoutSettingsCard";
import { SitePasswordLoginCard } from "../SitePasswordLoginCard/SitePasswordLoginCard";
import { messages } from "./messages";
+const stockAvailabilityWebhooks = [
+ WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_IN_CHANNEL,
+ WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_IN_CHANNEL,
+ WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_OUT_OF_STOCK_FOR_CLICK_AND_COLLECT,
+ WebhookEventTypeAsyncEnum.PRODUCT_VARIANT_BACK_IN_STOCK_FOR_CLICK_AND_COLLECT,
+];
+
+const stockAvailabilityDocsUrl =
+ "https://docs.saleor.io/developer/stock/overview#legacy-stock-availability";
+
interface SiteSettingsPageAddressFormData {
city: string;
companyName: string;
@@ -46,6 +59,7 @@ export interface SiteSettingsPageFormData extends SiteSettingsPageAddressFormDat
limitQuantityPerCheckout: number;
emailConfirmation: boolean;
useLegacyUpdateWebhookEmission: boolean;
+ useLegacyShippingZoneStockAvailability: boolean;
preserveAllAddressFields: boolean;
passwordLoginMode: PasswordLoginModeEnum;
}
@@ -101,6 +115,7 @@ const SiteSettingsPage = (props: SiteSettingsPageProps) => {
limitQuantityPerCheckout: shop?.limitQuantityPerCheckout ?? 0,
emailConfirmation: shop?.enableAccountConfirmationByEmail ?? false,
useLegacyUpdateWebhookEmission: shop?.useLegacyUpdateWebhookEmission ?? true,
+ useLegacyShippingZoneStockAvailability: shop?.useLegacyShippingZoneStockAvailability ?? true,
preserveAllAddressFields: shop?.preserveAllAddressFields ?? false,
passwordLoginMode: shop?.passwordLoginMode,
};
@@ -133,6 +148,9 @@ const SiteSettingsPage = (props: SiteSettingsPageProps) => {
const handlePreserveAddressFieldsChange = isEnabled => {
change({ target: { name: "preserveAllAddressFields", value: isEnabled } });
};
+ const handleLegacyStockAvailabilityChange = isEnabled => {
+ change({ target: { name: "useLegacyShippingZoneStockAvailability", value: isEnabled } });
+ };
return (
@@ -251,6 +269,62 @@ const SiteSettingsPage = (props: SiteSettingsPageProps) => {
+
+
+
+ {intl.formatMessage(messages.sectionStockAvailabilityTitle)}
+
+
+
+ (
+
+ {chunks}
+
+ ),
+ }}
+ />
+
+
+
+
+
+ {intl.formatMessage(messages.sectionStockAvailabilityHeader)}
+
+
+
+
+
+ {intl.formatMessage(messages.sectionStockAvailabilityHeader)}
+
+
+
+ {intl.formatMessage(messages.sectionStockAvailabilityWebhooksIntro)}
+
+
+ {stockAvailabilityWebhooks.map(name => (
+
+ {name}
+
+ ))}
+
+
+
+
+
+
+
Learn more.",
+ description: "section description",
+ },
+ sectionStockAvailabilityHeader: {
+ id: "6ZnMfQ",
+ defaultMessage: "Use legacy shipping zone stock availability",
+ description: "card header and checkbox label",
+ },
+ sectionStockAvailabilityWebhooksIntro: {
+ id: "wmybDi",
+ defaultMessage: "When disabled, the following channel-scoped stock webhooks become available:",
+ description: "intro to webhook list",
+ },
});
diff --git a/src/siteSettings/fixtures.ts b/src/siteSettings/fixtures.ts
index 2d7d81c55dd..92381b3582b 100644
--- a/src/siteSettings/fixtures.ts
+++ b/src/siteSettings/fixtures.ts
@@ -42,6 +42,7 @@ export const shop: SiteSettingsQuery["shop"] = {
limitQuantityPerCheckout: 50,
enableAccountConfirmationByEmail: true,
useLegacyUpdateWebhookEmission: true,
+ useLegacyShippingZoneStockAvailability: true,
preserveAllAddressFields: false,
passwordLoginMode: PasswordLoginModeEnum.ENABLED,
};
diff --git a/src/siteSettings/views/index.tsx b/src/siteSettings/views/index.tsx
index f3cf5a3cd7e..bef1ba96db6 100644
--- a/src/siteSettings/views/index.tsx
+++ b/src/siteSettings/views/index.tsx
@@ -72,6 +72,7 @@ const SiteSettings = () => {
enableAccountConfirmationByEmail: data.emailConfirmation,
limitQuantityPerCheckout: data.limitQuantityPerCheckout || null,
useLegacyUpdateWebhookEmission: data.useLegacyUpdateWebhookEmission,
+ useLegacyShippingZoneStockAvailability: data.useLegacyShippingZoneStockAvailability,
preserveAllAddressFields: data.preserveAllAddressFields,
passwordLoginMode: data.passwordLoginMode,
};