Add stock in product variant create view

This commit is contained in:
dominik-zeglen 2020-03-27 12:06:11 +01:00
parent 0eb7750321
commit d38fa42462
9 changed files with 139 additions and 132 deletions

View file

@ -15,6 +15,7 @@ import useFormset, {
} from "@saleor/hooks/useFormset";
import { getVariantAttributeInputFromProduct } from "@saleor/products/utils/data";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { maybe } from "../../../misc";
import { ProductVariantCreateData_product } from "../../types/ProductVariantCreateData";
import ProductVariantAttributes, {
@ -22,7 +23,7 @@ import ProductVariantAttributes, {
} from "../ProductVariantAttributes";
import ProductVariantNavigation from "../ProductVariantNavigation";
import ProductVariantPrice from "../ProductVariantPrice";
import ProductVariantStock from "../ProductVariantStock";
import ProductStocks, { ProductStockInput } from "../ProductStocks";
interface ProductVariantCreatePageFormData {
costPrice: string;
@ -30,35 +31,43 @@ interface ProductVariantCreatePageFormData {
priceOverride: string;
quantity: string;
sku: string;
trackInventory: boolean;
}
export interface ProductVariantCreatePageSubmitData
extends ProductVariantCreatePageFormData {
attributes: FormsetData<VariantAttributeInputData>;
stocks: ProductStockInput[];
}
interface ProductVariantCreatePageProps {
currencySymbol: string;
disabled: boolean;
errors: ProductErrorFragment[];
header: string;
loading: boolean;
product: ProductVariantCreateData_product;
saveButtonBarState: ConfirmButtonTransitionState;
warehouses: SearchWarehouses_search_edges_node[];
onBack: () => void;
onSubmit: (data: ProductVariantCreatePageSubmitData) => void;
onVariantClick: (variantId: string) => void;
onWarehouseEdit: () => void;
}
const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
currencySymbol,
disabled,
errors,
loading,
header,
product,
saveButtonBarState,
warehouses,
onBack,
onSubmit,
onVariantClick
onVariantClick,
onWarehouseEdit
}) => {
const intl = useIntl();
const attributeInput = React.useMemo(
@ -68,28 +77,33 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
const { change: changeAttributeData, data: attributes } = useFormset(
attributeInput
);
const { change: changeStockData, data: stocks, set: setStocks } = useFormset<
null
>([]);
React.useEffect(() => {
const newStocks = warehouses.map(warehouse => ({
data: null,
id: warehouse.id,
label: warehouse.name,
value: stocks.find(stock => stock.id === warehouse.id)?.value || 0
}));
setStocks(newStocks);
}, [JSON.stringify(warehouses)]);
const initialForm = {
attributes: maybe(
() =>
product.productType.variantAttributes.map(attribute => ({
name: attribute.name,
slug: attribute.slug,
values: [""]
})),
[]
),
const initialForm: ProductVariantCreatePageFormData = {
costPrice: "",
images: maybe(() => product.images.map(image => image.id)),
priceOverride: "",
quantity: "0",
sku: ""
sku: "",
trackInventory: true
};
const handleSubmit = (data: ProductVariantCreatePageFormData) =>
onSubmit({
...data,
attributes
attributes,
stocks
});
return (
@ -133,12 +147,14 @@ const ProductVariantCreatePage: React.FC<ProductVariantCreatePageProps> = ({
onChange={change}
/>
<CardSpacer />
<ProductVariantStock
<ProductStocks
data={data}
disabled={disabled}
onChange={changeStockData}
onFormDataChange={change}
errors={errors}
sku={data.sku}
quantity={data.quantity}
loading={loading}
onChange={change}
stocks={stocks}
onWarehousesEdit={onWarehouseEdit}
/>
</div>
</Grid>

View file

@ -1,97 +0,0 @@
import Card from "@material-ui/core/Card";
import CardContent from "@material-ui/core/CardContent";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import React from "react";
import { useIntl } from "react-intl";
import CardTitle from "@saleor/components/CardTitle";
import { getFormErrors, getProductErrorMessage } from "@saleor/utils/errors";
import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment";
const useStyles = makeStyles(
theme => ({
grid: {
display: "grid",
gridColumnGap: theme.spacing(2),
gridTemplateColumns: "1fr 1fr"
}
}),
{ name: "ProductVariantStock" }
);
interface ProductVariantStockProps {
errors: ProductErrorFragment[];
sku: string;
quantity: string;
stockAllocated?: number;
loading?: boolean;
onChange(event: any);
}
const ProductVariantStock: React.FC<ProductVariantStockProps> = props => {
const { errors, sku, quantity, stockAllocated, loading, onChange } = props;
const classes = useStyles(props);
const intl = useIntl();
const formErrors = getFormErrors(["quantity", "sku"], errors);
return (
<Card>
<CardTitle
title={intl.formatMessage({
defaultMessage: "Stock",
description: "product variant stock, section header"
})}
/>
<CardContent>
<div className={classes.grid}>
<div>
<TextField
error={!!formErrors.quantity}
name="quantity"
value={quantity}
label={intl.formatMessage({
defaultMessage: "Inventory",
description: "product variant stock"
})}
helperText={
getProductErrorMessage(formErrors.quantity, intl) ||
(!!stockAllocated &&
intl.formatMessage(
{
defaultMessage: "Allocated: {quantity}",
description: "variant allocated stock"
},
{
quantity: stockAllocated
}
))
}
onChange={onChange}
disabled={loading}
fullWidth
/>
</div>
<div>
<TextField
error={!!formErrors.sku}
helperText={getProductErrorMessage(formErrors.sku, intl)}
name="sku"
value={sku}
label={intl.formatMessage({
defaultMessage: "SKU (Stock Keeping Unit)"
})}
onChange={onChange}
disabled={loading}
fullWidth
/>
</div>
</div>
</CardContent>
</Card>
);
};
ProductVariantStock.displayName = "ProductVariantStock";
export default ProductVariantStock;

View file

@ -1,2 +0,0 @@
export { default } from "./ProductVariantStock";
export * from "./ProductVariantStock";

View file

@ -19,7 +19,8 @@ import {
productVariantAddPath,
productVariantEditPath,
ProductVariantEditUrlQueryParams,
ProductAddUrlQueryParams
ProductAddUrlQueryParams,
ProductVariantAddUrlQueryParams
} from "./urls";
import ProductCreateComponent from "./views/ProductCreate";
import ProductImageComponent from "./views/ProductImage";
@ -87,11 +88,17 @@ const ProductImage: React.FC<RouteComponentProps<any>> = ({
const ProductVariantCreate: React.FC<RouteComponentProps<any>> = ({
match
}) => (
}) => {
const qs = parseQs(location.search.substr(1));
const params: ProductVariantAddUrlQueryParams = qs;
return (
<ProductVariantCreateComponent
productId={decodeURIComponent(match.params.id)}
params={params}
/>
);
};
const ProductCreate: React.FC<RouteComponentProps> = ({ location }) => {
const qs = parseQs(location.search.substr(1));

View file

@ -303,6 +303,7 @@ export const productCreateMutation = gql`
$stockQuantity: Int
$seo: SeoInput
$stocks: [StockInput!]!
$trackInventory: Boolean!
) {
productCreate(
input: {
@ -320,6 +321,7 @@ export const productCreateMutation = gql`
quantity: $stockQuantity
seo: $seo
stocks: $stocks
trackInventory: $trackInventory
}
) {
errors: productErrors {

View file

@ -212,4 +212,5 @@ export interface ProductCreateVariables {
stockQuantity?: number | null;
seo?: SeoInput | null;
stocks: StockInput[];
trackInventory: boolean;
}

View file

@ -98,8 +98,17 @@ export const productVariantEditUrl = (
export const productVariantAddPath = (productId: string) =>
urlJoin(productSection, productId, "variant/add");
export const productVariantAddUrl = (productId: string) =>
productVariantAddPath(encodeURIComponent(productId));
export type ProductVariantAddUrlDialog = "edit-stocks";
export type ProductVariantAddUrlQueryParams = Dialog<
ProductVariantAddUrlDialog
>;
export const productVariantAddUrl = (
productId: string,
params?: ProductVariantAddUrlQueryParams
): string =>
productVariantAddPath(encodeURIComponent(productId)) +
"?" +
stringifyQs(params);
export const productImagePath = (productId: string, imageId: string) =>
urlJoin(productSection, productId, "image", imageId);

View file

@ -121,7 +121,8 @@ export const ProductCreateView: React.FC<ProductCreateViewProps> = ({
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
}))
})),
trackInventory: formData.trackInventory
}
});
};

View file

@ -7,24 +7,58 @@ import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop";
import NotFoundPage from "@saleor/components/NotFoundPage";
import { commonMessages } from "@saleor/intl";
import { decimal, maybe } from "../../misc";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@saleor/config";
import useWarehouseSearch from "@saleor/searches/useWarehouseSearch";
import { decimal } from "../../misc";
import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData
} from "../components/ProductVariantCreatePage";
import { TypedVariantCreateMutation } from "../mutations";
import { TypedProductVariantCreateQuery } from "../queries";
import { VariantCreate } from "../types/VariantCreate";
import { productUrl, productVariantEditUrl, productListUrl } from "../urls";
import {
productUrl,
productVariantEditUrl,
productListUrl,
productVariantAddUrl,
ProductVariantAddUrlDialog,
ProductVariantAddUrlQueryParams
} from "../urls";
import ProductWarehousesDialog from "../components/ProductWarehousesDialog";
interface ProductUpdateProps {
interface ProductVariantCreateProps {
params: ProductVariantAddUrlQueryParams;
productId: string;
}
export const ProductVariant: React.FC<ProductUpdateProps> = ({ productId }) => {
export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
params,
productId
}) => {
const navigate = useNavigator();
const notify = useNotifier();
const shop = useShop();
const intl = useIntl();
const {
loadMore: loadMoreWarehouses,
search: searchWarehouses,
result: searchWarehousesOpts
} = useWarehouseSearch({
variables: {
...DEFAULT_INITIAL_SEARCH_DATA,
first: 20
}
});
const [warehouses, setWarehouses] = React.useState<
SearchWarehouses_search_edges_node[]
>([]);
const [openModal, closeModal] = createDialogActionHandlers<
ProductVariantAddUrlDialog,
ProductVariantAddUrlQueryParams
>(navigate, params => productVariantAddUrl(productId, params), params);
return (
<TypedProductVariantCreateQuery displayLoader variables={{ id: productId }}>
@ -70,6 +104,10 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({ productId }) => {
product: productId,
quantity: parseInt(formData.quantity, 0),
sku: formData.sku,
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
})),
trackInventory: true
}
}
@ -88,7 +126,8 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({ productId }) => {
})}
/>
<ProductVariantCreatePage
currencySymbol={maybe(() => shop.defaultCurrency)}
currencySymbol={shop?.defaultCurrency}
disabled={productLoading}
errors={
variantCreateResult.data?.productVariantCreate.errors ||
[]
@ -103,6 +142,37 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({ productId }) => {
onSubmit={handleSubmit}
onVariantClick={handleVariantClick}
saveButtonBarState={variantCreateResult.status}
warehouses={warehouses}
onWarehouseEdit={() => openModal("edit-stocks")}
/>
<ProductWarehousesDialog
confirmButtonState="default"
disabled={false}
errors={[]}
onClose={closeModal}
open={params.action === "edit-stocks"}
warehouses={searchWarehousesOpts.data?.search.edges.map(
edge => edge.node
)}
warehousesWithStocks={warehouses.map(
warehouse => warehouse.id
)}
onConfirm={data => {
setWarehouses(
[
...warehouses,
...data.added.map(
addedId =>
searchWarehousesOpts.data.search.edges.find(
edge => edge.node.id === addedId
).node
)
].filter(
warehouse => !data.removed.includes(warehouse.id)
)
);
closeModal();
}}
/>
</>
);