diff --git a/src/products/components/ProductStocks/ProductStocks.tsx b/src/products/components/ProductStocks/ProductStocks.tsx index 54c770223..1a2063b8b 100644 --- a/src/products/components/ProductStocks/ProductStocks.tsx +++ b/src/products/components/ProductStocks/ProductStocks.tsx @@ -180,6 +180,7 @@ const ProductStocks: React.FC = ({ ; + stocks: ProductStockInput[]; } interface ProductVariantPageProps { @@ -44,6 +48,7 @@ interface ProductVariantPageProps { loading?: boolean; placeholderImage?: string; header: string; + onWarehousesEdit: () => void; onAdd(); onBack(); onDelete(); @@ -64,15 +69,20 @@ const ProductVariantPage: React.FC = ({ onDelete, onImageSelect, onSubmit, + onWarehousesEdit, onVariantClick }) => { const attributeInput = React.useMemo( () => getAttributeInputFromVariant(variant), [variant] ); + const stockInput = React.useMemo(() => getStockInputFromVariant(variant), [ + variant + ]); const { change: changeAttributeData, data: attributes } = useFormset( attributeInput ); + const { change: changeStockData, data: stocks } = useFormset(stockInput); const [isModalOpened, setModalStatus] = React.useState(false); const toggleModal = () => setModalStatus(!isModalOpened); @@ -92,14 +102,15 @@ const ProductVariantPage: React.FC = ({ const initialForm: ProductVariantPageFormData = { costPrice: maybe(() => variant.costPrice.amount.toString(), ""), priceOverride: maybe(() => variant.priceOverride.amount.toString(), ""), - quantity: maybe(() => variant.quantity.toString(), "0"), - sku: maybe(() => variant.sku, "") + sku: maybe(() => variant.sku, ""), + trackInventory: variant?.trackInventory }; const handleSubmit = (data: ProductVariantPageFormData) => onSubmit({ ...data, - attributes + attributes, + stocks }); return ( @@ -164,15 +175,17 @@ const ProductVariantPage: React.FC = ({ onChange={change} /> - { + triggerChange(); + changeStockData(id, value); + }} + onFormDataChange={change} + onWarehousesEdit={onWarehousesEdit} /> diff --git a/src/products/mutations.ts b/src/products/mutations.ts index 48c625ec2..a8252094d 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -355,6 +355,7 @@ export const TypedVariantDeleteMutation = TypedMutation< >(variantDeleteMutation); export const variantUpdateMutation = gql` + ${bulkStockErrorFragment} ${fragmentVariant} ${productErrorFragment} mutation VariantUpdate( @@ -365,6 +366,7 @@ export const variantUpdateMutation = gql` $sku: String $quantity: Int $trackInventory: Boolean! + $stocks: [StockInput!]! ) { productVariantUpdate( id: $id @@ -384,6 +386,14 @@ export const variantUpdateMutation = gql` ...ProductVariant } } + productVariantStocksUpdate(stocks: $stocks, variantId: $id) { + errors: bulkStockErrors { + ...BulkStockErrorFragment + } + productVariant { + ...ProductVariant + } + } } `; export const TypedVariantUpdateMutation = TypedMutation< diff --git a/src/products/types/VariantUpdate.ts b/src/products/types/VariantUpdate.ts index c0deeabd3..27546842c 100644 --- a/src/products/types/VariantUpdate.ts +++ b/src/products/types/VariantUpdate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput, ProductErrorCode } from "./../../types/globalTypes"; +import { AttributeValueInput, StockInput, ProductErrorCode } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: VariantUpdate @@ -130,8 +130,132 @@ export interface VariantUpdate_productVariantUpdate { productVariant: VariantUpdate_productVariantUpdate_productVariant | null; } +export interface VariantUpdate_productVariantStocksUpdate_errors { + __typename: "BulkStockError"; + code: ProductErrorCode; + field: string | null; + index: number | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute { + __typename: "Attribute"; + id: string; + name: string | null; + slug: string | null; + valueRequired: boolean; + values: (VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute_values | null)[] | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes_values { + __typename: "AttributeValue"; + id: string; + name: string | null; + slug: string | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_attributes { + __typename: "SelectedAttribute"; + attribute: VariantUpdate_productVariantStocksUpdate_productVariant_attributes_attribute; + values: (VariantUpdate_productVariantStocksUpdate_productVariant_attributes_values | null)[]; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_costPrice { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_images { + __typename: "ProductImage"; + id: string; + url: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_priceOverride { + __typename: "Money"; + amount: number; + currency: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_product_images { + __typename: "ProductImage"; + id: string; + alt: string; + sortOrder: number | null; + url: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_product_thumbnail { + __typename: "Image"; + url: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_product_variants_images { + __typename: "ProductImage"; + id: string; + url: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_product_variants { + __typename: "ProductVariant"; + id: string; + name: string; + sku: string; + images: (VariantUpdate_productVariantStocksUpdate_productVariant_product_variants_images | null)[] | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_product { + __typename: "Product"; + id: string; + images: (VariantUpdate_productVariantStocksUpdate_productVariant_product_images | null)[] | null; + name: string; + thumbnail: VariantUpdate_productVariantStocksUpdate_productVariant_product_thumbnail | null; + variants: (VariantUpdate_productVariantStocksUpdate_productVariant_product_variants | null)[] | null; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_stocks_warehouse { + __typename: "Warehouse"; + id: string; + name: string; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant_stocks { + __typename: "Stock"; + id: string; + quantity: number; + warehouse: VariantUpdate_productVariantStocksUpdate_productVariant_stocks_warehouse; +} + +export interface VariantUpdate_productVariantStocksUpdate_productVariant { + __typename: "ProductVariant"; + id: string; + attributes: VariantUpdate_productVariantStocksUpdate_productVariant_attributes[]; + costPrice: VariantUpdate_productVariantStocksUpdate_productVariant_costPrice | null; + images: (VariantUpdate_productVariantStocksUpdate_productVariant_images | null)[] | null; + name: string; + priceOverride: VariantUpdate_productVariantStocksUpdate_productVariant_priceOverride | null; + product: VariantUpdate_productVariantStocksUpdate_productVariant_product; + sku: string; + stocks: (VariantUpdate_productVariantStocksUpdate_productVariant_stocks | null)[] | null; + trackInventory: boolean; +} + +export interface VariantUpdate_productVariantStocksUpdate { + __typename: "ProductVariantStocksUpdate"; + errors: VariantUpdate_productVariantStocksUpdate_errors[]; + productVariant: VariantUpdate_productVariantStocksUpdate_productVariant | null; +} + export interface VariantUpdate { productVariantUpdate: VariantUpdate_productVariantUpdate | null; + productVariantStocksUpdate: VariantUpdate_productVariantStocksUpdate | null; } export interface VariantUpdateVariables { @@ -142,4 +266,5 @@ export interface VariantUpdateVariables { sku?: string | null; quantity?: number | null; trackInventory: boolean; + stocks: StockInput[]; } diff --git a/src/products/urls.ts b/src/products/urls.ts index 17ecded15..4229e7871 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -80,8 +80,10 @@ export const productUrl = (id: string, params?: ProductUrlQueryParams) => export const productVariantEditPath = (productId: string, variantId: string) => urlJoin(productSection, productId, "variant", variantId); -export type ProductVariantEditUrlDialog = "remove"; -export type ProductVariantEditUrlQueryParams = Dialog<"remove">; +export type ProductVariantEditUrlDialog = "edit-stocks" | "remove"; +export type ProductVariantEditUrlQueryParams = Dialog< + ProductVariantEditUrlDialog +>; export const productVariantEditUrl = ( productId: string, variantId: string, diff --git a/src/products/utils/data.ts b/src/products/utils/data.ts index 23c19429c..45be5ee4b 100644 --- a/src/products/utils/data.ts +++ b/src/products/utils/data.ts @@ -103,6 +103,19 @@ export function getAttributeInputFromVariant( ); } +export function getStockInputFromVariant( + variant: ProductVariant +): ProductStockInput[] { + return ( + variant?.stocks.map(stock => ({ + data: null, + id: stock.warehouse.id, + label: stock.warehouse.name, + value: stock.quantity.toString() + })) || [] + ); +} + export function getVariantAttributeInputFromProduct( product: ProductVariantCreateData_product ): VariantAttributeInput[] { diff --git a/src/products/views/ProductVariant.tsx b/src/products/views/ProductVariant.tsx index 6a68c2d49..fb3c4a28f 100644 --- a/src/products/views/ProductVariant.tsx +++ b/src/products/views/ProductVariant.tsx @@ -7,7 +7,10 @@ import useNavigator from "@saleor/hooks/useNavigator"; import useNotifier from "@saleor/hooks/useNotifier"; import { commonMessages } from "@saleor/intl"; import NotFoundPage from "@saleor/components/NotFoundPage"; -import { decimal, maybe } from "../../misc"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config"; +import useWarehouseSearch from "@saleor/searches/useWarehouseSearch"; +import { decimal } from "../../misc"; import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog"; import ProductVariantPage, { ProductVariantPageSubmitData @@ -22,8 +25,11 @@ import { productUrl, productVariantAddUrl, productVariantEditUrl, - ProductVariantEditUrlQueryParams + ProductVariantEditUrlQueryParams, + ProductVariantEditUrlDialog } from "../urls"; +import ProductWarehousesDialog from "../components/ProductWarehousesDialog"; +import { useAddOrRemoveStocks } from "../mutations"; interface ProductUpdateProps { variantId: string; @@ -46,6 +52,40 @@ export const ProductVariant: React.FC = ({ setErrors([]); }, [variantId]); + const { + loadMore: loadMoreWarehouses, + search: searchWarehouses, + result: searchWarehousesOpts + } = useWarehouseSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 20 + } + }); + + const [addOrRemoveStocks, addOrRemoveStocksOpts] = useAddOrRemoveStocks({ + onCompleted: data => { + if ( + data.productVariantStocksCreate.errors.length === 0 && + data.productVariantStocksDelete.errors.length === 0 + ) { + notify({ + text: intl.formatMessage(commonMessages.savedChanges) + }); + closeModal(); + } + } + }); + + const [openModal, closeModal] = createDialogActionHandlers< + ProductVariantEditUrlDialog, + ProductVariantEditUrlQueryParams + >( + navigate, + params => productVariantEditUrl(productId, variantId, params), + params + ); + const handleBack = () => navigate(productUrl(productId)); return ( @@ -107,14 +147,14 @@ export const ProductVariant: React.FC = ({ return ( <> - data.productVariant.name)} /> + navigate(productVariantAddUrl(productId))} onBack={handleBack} onDelete={() => @@ -125,25 +165,27 @@ export const ProductVariant: React.FC = ({ ) } onImageSelect={handleImageSelect} - onSubmit={(data: ProductVariantPageSubmitData) => { - if (variant) { - updateVariant.mutate({ - attributes: data.attributes.map(attribute => ({ - id: attribute.id, - values: [attribute.value] - })), - costPrice: decimal(data.costPrice), - id: variantId, - priceOverride: decimal(data.priceOverride), - quantity: parseInt(data.quantity, 0), - sku: data.sku, - trackInventory: true // FIXME: missing in UI - }); - } - }} + onSubmit={(data: ProductVariantPageSubmitData) => + updateVariant.mutate({ + attributes: data.attributes.map(attribute => ({ + id: attribute.id, + values: [attribute.value] + })), + costPrice: decimal(data.costPrice), + id: variantId, + priceOverride: decimal(data.priceOverride), + sku: data.sku, + stocks: data.stocks.map(stock => ({ + quantity: parseInt(stock.value, 10), + warehouse: stock.id + })), + trackInventory: data.trackInventory + }) + } onVariantClick={variantId => { navigate(productVariantEditUrl(productId, variantId)); }} + onWarehousesEdit={() => openModal("edit-stocks")} /> = ({ }) } open={params.action === "remove"} - name={maybe(() => data.productVariant.name)} + name={data?.productVariant?.name} + /> + edge.node + )} + warehousesWithStocks={ + variant?.stocks.map(stock => stock.warehouse.id) || [] + } + onConfirm={data => + addOrRemoveStocks({ + variables: { + add: data.added.map(id => ({ + quantity: 0, + warehouse: id + })), + remove: data.removed, + variantId + } + }) + } /> );