diff --git a/locale/defaultMessages.json b/locale/defaultMessages.json index 105c9bf12..5c00097a1 100644 --- a/locale/defaultMessages.json +++ b/locale/defaultMessages.json @@ -5897,6 +5897,10 @@ "context": "webhook events", "string": "Expand or restrict webhooks permissions to register certain events in Saleor system." }, + "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3316426878": { + "context": "event", + "string": "Product updated" + }, "src_dot_webhooks_dot_components_dot_WebhookEvents_dot_3345061702": { "context": "event", "string": "Order fully paid" diff --git a/schema.graphql b/schema.graphql index e2953571b..abe05671b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -2640,6 +2640,7 @@ type Mutation { productClearPrivateMetadata(id: ID!, input: MetaPath!): ProductClearPrivateMeta @deprecated(reason: "Use the `deletePrivateMetadata` mutation instead. This field will be removed after 2020-07-31.") productSetAvailabilityForPurchase(isAvailable: Boolean!, productId: ID!, startDate: Date): ProductSetAvailabilityForPurchase productImageCreate(input: ProductImageCreateInput!): ProductImageCreate + productVariantReorder(moves: [ReorderInput]!, productId: ID!): ProductVariantReorder productImageDelete(id: ID!): ProductImageDelete productImageBulkDelete(ids: [ID]!): ProductImageBulkDelete productImageReorder(imagesIds: [ID]!, productId: ID!): ProductImageReorder @@ -2999,6 +3000,7 @@ enum OrderErrorCode { REQUIRED SHIPPING_METHOD_NOT_APPLICABLE SHIPPING_METHOD_REQUIRED + TAX_ERROR UNIQUE VOID_INACTIVE_PAYMENT ZERO_QUANTITY @@ -4174,6 +4176,12 @@ input ProductVariantInput { weight: WeightScalar } +type ProductVariantReorder { + errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") + product: Product + productErrors: [ProductError!]! +} + type ProductVariantStocksCreate { errors: [Error!]! @deprecated(reason: "Use typed errors with error codes. This field will be removed after 2020-07-31.") productVariant: ProductVariant @@ -5600,6 +5608,7 @@ enum WebhookEventTypeEnum { INVOICE_SENT CUSTOMER_CREATED PRODUCT_CREATED + PRODUCT_UPDATED CHECKOUT_QUANTITY_CHANGED CHECKOUT_CREATED CHECKOUT_UPDATED @@ -5622,6 +5631,7 @@ enum WebhookSampleEventTypeEnum { INVOICE_SENT CUSTOMER_CREATED PRODUCT_CREATED + PRODUCT_UPDATED CHECKOUT_QUANTITY_CHANGED CHECKOUT_CREATED CHECKOUT_UPDATED diff --git a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx index 249f03af6..39ac8e6b2 100644 --- a/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx +++ b/src/products/components/ProductVariantNavigation/ProductVariantNavigation.tsx @@ -116,7 +116,7 @@ const ProductVariantNavigation: React.FC = props ) : ( - + (productSetAvailabilityForPurchase); + +const productVariantReorder = gql` + ${productErrorFragment} + ${productFragmentDetails} + mutation ProductVariantReorder($move: ReorderInput!, $productId: ID!) { + productVariantReorder(moves: [$move], productId: $productId) { + errors: productErrors { + ...ProductErrorFragment + } + product { + ...Product + } + } + } +`; +export const useProductVariantReorderMutation = makeMutation< + ProductVariantReorder, + ProductVariantReorderVariables +>(productVariantReorder); diff --git a/src/products/types/ProductVariantReorder.ts b/src/products/types/ProductVariantReorder.ts new file mode 100644 index 000000000..8c5f1741f --- /dev/null +++ b/src/products/types/ProductVariantReorder.ts @@ -0,0 +1,236 @@ +/* tslint:disable */ +/* eslint-disable */ +// This file was automatically generated and should not be edited. + +import { ReorderInput, ProductErrorCode, AttributeInputTypeEnum, WeightUnitsEnum } from "./../../types/globalTypes"; + +// ==================================================== +// GraphQL mutation operation: ProductVariantReorder +// ==================================================== + +export interface ProductVariantReorder_productVariantReorder_errors { + __typename: "ProductError"; + code: ProductErrorCode; + field: string | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_attributes_attribute { + __typename: "Attribute"; + id: string; + slug: string | null; + name: string | null; + inputType: AttributeInputTypeEnum | null; + valueRequired: boolean; + values: (ProductVariantReorder_productVariantReorder_product_attributes_attribute_values | null)[] | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_attributes { + __typename: "SelectedAttribute"; + attribute: ProductVariantReorder_productVariantReorder_product_attributes_attribute; + values: (ProductVariantReorder_productVariantReorder_product_attributes_values | null)[]; +} + +export interface ProductVariantReorder_productVariantReorder_product_productType_variantAttributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_productType_variantAttributes { + __typename: "Attribute"; + id: string; + name: string | null; + values: (ProductVariantReorder_productVariantReorder_product_productType_variantAttributes_values | null)[] | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_productType { + __typename: "ProductType"; + id: string; + variantAttributes: (ProductVariantReorder_productVariantReorder_product_productType_variantAttributes | null)[] | null; + name: string; + hasVariants: boolean; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start_gross { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start { + __typename: "TaxedMoney"; + gross: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start_gross; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop_gross { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop { + __typename: "TaxedMoney"; + gross: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop_gross; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted { + __typename: "TaxedMoneyRange"; + start: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_start | null; + stop: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted_stop | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_pricing { + __typename: "ProductPricingInfo"; + priceRangeUndiscounted: ProductVariantReorder_productVariantReorder_product_pricing_priceRangeUndiscounted | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_metadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_privateMetadata { + __typename: "MetadataItem"; + key: string; + value: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_category { + __typename: "Category"; + id: string; + name: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_collections { + __typename: "Collection"; + id: string; + name: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_margin { + __typename: "Margin"; + start: number | null; + stop: number | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_purchaseCost_start { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_purchaseCost_stop { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_purchaseCost { + __typename: "MoneyRange"; + start: ProductVariantReorder_productVariantReorder_product_purchaseCost_start | null; + stop: ProductVariantReorder_productVariantReorder_product_purchaseCost_stop | null; +} + +export interface ProductVariantReorder_productVariantReorder_product_images { + __typename: "ProductImage"; + id: string; + alt: string; + sortOrder: number | null; + url: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_variants_price { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_variants_stocks_warehouse { + __typename: "Warehouse"; + id: string; + name: string; +} + +export interface ProductVariantReorder_productVariantReorder_product_variants_stocks { + __typename: "Stock"; + id: string; + quantity: number; + quantityAllocated: number; + warehouse: ProductVariantReorder_productVariantReorder_product_variants_stocks_warehouse; +} + +export interface ProductVariantReorder_productVariantReorder_product_variants { + __typename: "ProductVariant"; + id: string; + sku: string; + name: string; + price: ProductVariantReorder_productVariantReorder_product_variants_price | null; + margin: number | null; + stocks: (ProductVariantReorder_productVariantReorder_product_variants_stocks | null)[] | null; + trackInventory: boolean; +} + +export interface ProductVariantReorder_productVariantReorder_product_weight { + __typename: "Weight"; + unit: WeightUnitsEnum; + value: number; +} + +export interface ProductVariantReorder_productVariantReorder_product { + __typename: "Product"; + id: string; + attributes: ProductVariantReorder_productVariantReorder_product_attributes[]; + productType: ProductVariantReorder_productVariantReorder_product_productType; + pricing: ProductVariantReorder_productVariantReorder_product_pricing | null; + metadata: (ProductVariantReorder_productVariantReorder_product_metadata | null)[]; + privateMetadata: (ProductVariantReorder_productVariantReorder_product_privateMetadata | null)[]; + name: string; + descriptionJson: any; + seoTitle: string | null; + seoDescription: string | null; + category: ProductVariantReorder_productVariantReorder_product_category | null; + collections: (ProductVariantReorder_productVariantReorder_product_collections | null)[] | null; + margin: ProductVariantReorder_productVariantReorder_product_margin | null; + purchaseCost: ProductVariantReorder_productVariantReorder_product_purchaseCost | null; + isAvailableForPurchase: boolean | null; + isAvailable: boolean | null; + isPublished: boolean; + chargeTaxes: boolean; + publicationDate: any | null; + images: (ProductVariantReorder_productVariantReorder_product_images | null)[] | null; + variants: (ProductVariantReorder_productVariantReorder_product_variants | null)[] | null; + weight: ProductVariantReorder_productVariantReorder_product_weight | null; + availableForPurchase: any | null; + visibleInListings: boolean; +} + +export interface ProductVariantReorder_productVariantReorder { + __typename: "ProductVariantReorder"; + errors: ProductVariantReorder_productVariantReorder_errors[]; + product: ProductVariantReorder_productVariantReorder_product | null; +} + +export interface ProductVariantReorder { + productVariantReorder: ProductVariantReorder_productVariantReorder | null; +} + +export interface ProductVariantReorderVariables { + move: ReorderInput; + productId: string; +} diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 33b12df25..8aeef201c 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -20,6 +20,7 @@ import { useProductSetAvailabilityForPurchase, useProductUpdateMutation, useProductVariantBulkDeleteMutation, + useProductVariantReorderMutation, useSimpleProductUpdateMutation } from "@saleor/products/mutations"; import useCategorySearch from "@saleor/searches/useCategorySearch"; @@ -52,7 +53,8 @@ import { import { createImageReorderHandler, createImageUploadHandler, - createUpdateHandler + createUpdateHandler, + createVariantReorderHandler } from "./handlers"; interface ProductUpdateProps { @@ -232,12 +234,22 @@ export const ProductUpdate: React.FC = ({ id, params }) => { reorderProductImages({ variables }) ); + const [ + reorderProductVariants, + reorderProductVariantsOpts + ] = useProductVariantReorderMutation({}); + + const handleVariantReorder = createVariantReorderHandler(product, variables => + reorderProductVariants({ variables }) + ); + const disableFormSave = createProductImageOpts.loading || deleteProductOpts.loading || reorderProductImagesOpts.loading || updateProductOpts.loading || productAvailabilityOpts.loading || + reorderProductVariantsOpts.loading || loading; const formTransitionState = getMutationState( @@ -289,7 +301,7 @@ export const ProductUpdate: React.FC = ({ id, params }) => { onVariantsAdd={() => navigate(productVariantCreatorUrl(id))} onVariantShow={variantId => () => navigate(productVariantEditUrl(product.id, variantId))} - onVariantReorder={() => undefined} // TODO: ... + onVariantReorder={handleVariantReorder} onImageUpload={handleImageUpload} onImageEdit={handleImageEdit} onImageDelete={handleImageDelete} diff --git a/src/products/views/ProductUpdate/handlers.ts b/src/products/views/ProductUpdate/handlers.ts index 1d1cdd56f..6e3de1180 100644 --- a/src/products/views/ProductUpdate/handlers.ts +++ b/src/products/views/ProductUpdate/handlers.ts @@ -14,6 +14,9 @@ import { ProductUpdate, ProductUpdateVariables } from "@saleor/products/types/ProductUpdate"; +import { ProductVariantCreateData_product } from "@saleor/products/types/ProductVariantCreateData"; +import { ProductVariantDetails_productVariant_product } from "@saleor/products/types/ProductVariantDetails"; +import { ProductVariantReorderVariables } from "@saleor/products/types/ProductVariantReorder"; import { SimpleProductUpdate, SimpleProductUpdateVariables @@ -134,3 +137,21 @@ export function createImageReorderHandler( }); }; } + +export function createVariantReorderHandler( + product: + | ProductDetails_product + | ProductVariantDetails_productVariant_product + | ProductVariantCreateData_product, + reorderProductVariants: (variables: ProductVariantReorderVariables) => void +) { + return ({ newIndex, oldIndex }: ReorderEvent) => { + reorderProductVariants({ + move: { + id: product.variants[oldIndex].id, + sortOrder: newIndex - oldIndex + }, + productId: product.id + }); + }; +} diff --git a/src/products/views/ProductVariant.tsx b/src/products/views/ProductVariant.tsx index 36fac92c9..d090a2f70 100644 --- a/src/products/views/ProductVariant.tsx +++ b/src/products/views/ProductVariant.tsx @@ -21,6 +21,7 @@ import ProductVariantPage, { ProductVariantPageSubmitData } from "../components/ProductVariantPage"; import { + useProductVariantReorderMutation, useVariantDeleteMutation, useVariantImageAssignMutation, useVariantImageUnassignMutation, @@ -36,6 +37,7 @@ import { ProductVariantEditUrlQueryParams } from "../urls"; import { mapFormsetStockToStockInput } from "../utils/data"; +import { createVariantReorderHandler } from "./ProductUpdate/handlers"; interface ProductUpdateProps { variantId: string; @@ -120,12 +122,23 @@ export const ProductVariant: React.FC = ({ return ; } + const [ + reorderProductVariants, + reorderProductVariantsOpts + ] = useProductVariantReorderMutation({}); + + const handleVariantReorder = createVariantReorderHandler( + variant?.product, + variables => reorderProductVariants({ variables }) + ); + const disableFormSave = loading || deleteVariantOpts.loading || updateVariantOpts.loading || assignImageOpts.loading || - unassignImageOpts.loading; + unassignImageOpts.loading || + reorderProductVariantsOpts.loading; const handleImageSelect = (id: string) => () => { if (variant) { @@ -202,7 +215,7 @@ export const ProductVariant: React.FC = ({ onVariantClick={variantId => { navigate(productVariantEditUrl(productId, variantId)); }} - onVariantReorder={() => undefined} // TODO: ... + onVariantReorder={handleVariantReorder} /> = ({ return navigate(productListUrl())} />; } + const [ + reorderProductVariants, + reorderProductVariantsOpts + ] = useProductVariantReorderMutation({}); + + const handleVariantReorder = createVariantReorderHandler(product, variables => + reorderProductVariants({ variables }) + ); + const handleBack = () => navigate(productUrl(productId)); const handleCreate = async (formData: ProductVariantCreatePageSubmitData) => { const result = await variantCreate({ @@ -104,7 +117,10 @@ export const ProductVariant: React.FC = ({ const handleVariantClick = (id: string) => navigate(productVariantEditUrl(productId, id)); - const disableForm = productLoading || variantCreateResult.loading; + const disableForm = + productLoading || + variantCreateResult.loading || + reorderProductVariantsOpts.loading; return ( <> @@ -126,7 +142,7 @@ export const ProductVariant: React.FC = ({ onBack={handleBack} onSubmit={handleSubmit} onVariantClick={handleVariantClick} - onVariantReorder={() => undefined} // TODO: ... + onVariantReorder={handleVariantReorder} saveButtonBarState={variantCreateResult.status} warehouses={ warehouses.data?.warehouses.edges.map(edge => edge.node) || [] diff --git a/src/types/globalTypes.ts b/src/types/globalTypes.ts index 7430a65da..5d626b4df 100644 --- a/src/types/globalTypes.ts +++ b/src/types/globalTypes.ts @@ -548,6 +548,7 @@ export enum OrderErrorCode { REQUIRED = "REQUIRED", SHIPPING_METHOD_NOT_APPLICABLE = "SHIPPING_METHOD_NOT_APPLICABLE", SHIPPING_METHOD_REQUIRED = "SHIPPING_METHOD_REQUIRED", + TAX_ERROR = "TAX_ERROR", UNIQUE = "UNIQUE", VOID_INACTIVE_PAYMENT = "VOID_INACTIVE_PAYMENT", ZERO_QUANTITY = "ZERO_QUANTITY", @@ -905,6 +906,7 @@ export enum WebhookEventTypeEnum { ORDER_FULLY_PAID = "ORDER_FULLY_PAID", ORDER_UPDATED = "ORDER_UPDATED", PRODUCT_CREATED = "PRODUCT_CREATED", + PRODUCT_UPDATED = "PRODUCT_UPDATED", } export enum WebhookSortField { diff --git a/src/webhooks/components/WebhookEvents/WebhookEvents.tsx b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx index 7bae16ebb..8578fa752 100644 --- a/src/webhooks/components/WebhookEvents/WebhookEvents.tsx +++ b/src/webhooks/components/WebhookEvents/WebhookEvents.tsx @@ -68,6 +68,10 @@ const WebhookEvents: React.FC = ({ defaultMessage: "Product created", description: "event" }), + [WebhookEventTypeEnum.PRODUCT_UPDATED]: intl.formatMessage({ + defaultMessage: "Product updated", + description: "event" + }), [WebhookEventTypeEnum.CHECKOUT_QUANTITY_CHANGED]: intl.formatMessage({ defaultMessage: "Changed quantity in checkout", description: "event"