Add variant stock management
This commit is contained in:
parent
4b661c9bff
commit
0eb7750321
7 changed files with 274 additions and 38 deletions
|
@ -180,6 +180,7 @@ const ProductStocks: React.FC<ProductStocksProps> = ({
|
|||
<TableCell className={classes.colQuantity}>
|
||||
<TextField
|
||||
className={classes.inputComponent}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
inputProps={{
|
||||
className: classes.input,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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<
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue