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, };