Add stock in product variant create view
This commit is contained in:
parent
0eb7750321
commit
d38fa42462
9 changed files with 139 additions and 132 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -1,2 +0,0 @@
|
|||
export { default } from "./ProductVariantStock";
|
||||
export * from "./ProductVariantStock";
|
|
@ -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
|
||||
}) => (
|
||||
<ProductVariantCreateComponent
|
||||
productId={decodeURIComponent(match.params.id)}
|
||||
/>
|
||||
);
|
||||
}) => {
|
||||
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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -212,4 +212,5 @@ export interface ProductCreateVariables {
|
|||
stockQuantity?: number | null;
|
||||
seo?: SeoInput | null;
|
||||
stocks: StockInput[];
|
||||
trackInventory: boolean;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue