Add variant stock management

This commit is contained in:
dominik-zeglen 2020-03-27 11:40:34 +01:00
parent 4b661c9bff
commit 0eb7750321
7 changed files with 274 additions and 38 deletions

View file

@ -180,6 +180,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
<TableCell className={classes.colQuantity}>
<TextField
className={classes.inputComponent}
disabled={disabled}
fullWidth
inputProps={{
className: classes.input,

View file

@ -13,7 +13,10 @@ import useFormset, {
FormsetData
} from "@saleor/hooks/useFormset";
import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate";
import { getAttributeInputFromVariant } from "@saleor/products/utils/data";
import {
getAttributeInputFromVariant,
getStockInputFromVariant
} from "@saleor/products/utils/data";
import { maybe } from "../../../misc";
import { ProductVariant } from "../../types/ProductVariant";
import ProductVariantAttributes, {
@ -23,18 +26,19 @@ import ProductVariantImages from "../ProductVariantImages";
import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog";
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantStock from "../ProductVariantStock";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
export interface ProductVariantPageFormData {
costPrice: string;
priceOverride: string;
quantity: string;
sku: string;
trackInventory: boolean;
}
export interface ProductVariantPageSubmitData
extends ProductVariantPageFormData {
attributes: FormsetData<VariantAttributeInputData, string>;
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<ProductVariantPageProps> = ({
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<ProductVariantPageProps> = ({
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<ProductVariantPageProps> = ({
onChange={change}
/>
<CardSpacer />
<ProductVariantStock
<ProductStocks
data={data}
disabled={loading}
errors={errors}
sku={data.sku}
quantity={data.quantity}
stockAllocated={
variant ? variant.quantityAllocated : undefined
}
loading={loading}
onChange={change}
stocks={stocks}
onChange={(id, value) => {
triggerChange();
changeStockData(id, value);
}}
onFormDataChange={change}
onWarehousesEdit={onWarehousesEdit}
/>
</div>
</Grid>

View file

@ -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<

View file

@ -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[];
}

View file

@ -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,

View file

@ -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[] {

View file

@ -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<ProductUpdateProps> = ({
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<ProductUpdateProps> = ({
return (
<>
<WindowTitle title={maybe(() => data.productVariant.name)} />
<WindowTitle title={data?.productVariant?.name} />
<ProductVariantPage
errors={errors}
saveButtonBarState={updateVariant.opts.status}
loading={disableFormSave}
placeholderImage={placeholderImg}
variant={variant}
header={variant ? variant.name || variant.sku : undefined}
header={variant?.name || variant?.sku}
onAdd={() => navigate(productVariantAddUrl(productId))}
onBack={handleBack}
onDelete={() =>
@ -125,25 +165,27 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
)
}
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")}
/>
<ProductVariantDeleteDialog
confirmButtonState={deleteVariant.opts.status}
@ -156,7 +198,37 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
})
}
open={params.action === "remove"}
name={maybe(() => data.productVariant.name)}
name={data?.productVariant?.name}
/>
<ProductWarehousesDialog
confirmButtonState={addOrRemoveStocksOpts.status}
disabled={addOrRemoveStocksOpts.loading}
errors={[
...(addOrRemoveStocksOpts.data?.productVariantStocksCreate
.errors || []),
...(addOrRemoveStocksOpts.data?.productVariantStocksDelete
.errors || [])
]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => 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
}
})
}
/>
</>
);