Merge pull request #667 from mirumee/ref/product-update-hooks

Use hooks instead of containers with render props in product mutations
This commit is contained in:
Dominik Żegleń 2020-08-25 13:13:15 +02:00 committed by GitHub
commit 03f03d436b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1156 additions and 1479 deletions

View file

@ -27,6 +27,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Add warehouse choice - #646 by @dominik-zeglen - Add warehouse choice - #646 by @dominik-zeglen
- Fix user management modal actions - #637 by @eaglesemanation - Fix user management modal actions - #637 by @eaglesemanation
- Fix navigator button rendering on safari browser - #656 by @dominik-zeglen - Fix navigator button rendering on safari browser - #656 by @dominik-zeglen
- Use hooks instead of containers with render props in product mutations - #667 by @dominik-zeglen
## 2.10.1 ## 2.10.1

View file

@ -17,8 +17,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import { PAGINATE_BY } from "../../config"; import { PAGINATE_BY } from "../../config";
import { maybe } from "../../misc"; import { maybe } from "../../misc";
import { TypedProductBulkDeleteMutation } from "../../products/mutations"; import { useProductBulkDeleteMutation } from "../../products/mutations";
import { productBulkDelete } from "../../products/types/productBulkDelete";
import { productAddUrl, productUrl } from "../../products/urls"; import { productAddUrl, productUrl } from "../../products/urls";
import { CategoryInput } from "../../types/globalTypes"; import { CategoryInput } from "../../types/globalTypes";
import { import {
@ -129,6 +128,23 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
onCompleted: handleBulkCategoryDelete onCompleted: handleBulkCategoryDelete
}); });
const [
productBulkDelete,
productBulkDeleteOpts
] = useProductBulkDeleteMutation({
onCompleted: data => {
if (data.productBulkDelete.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
}
}
});
const changeTab = (tabName: CategoryPageTab) => { const changeTab = (tabName: CategoryPageTab) => {
reset(); reset();
navigate( navigate(
@ -143,18 +159,6 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
CategoryUrlQueryParams CategoryUrlQueryParams
>(navigate, params => categoryUrl(id, params), params); >(navigate, params => categoryUrl(id, params), params);
const handleBulkProductDelete = (data: productBulkDelete) => {
if (data.productBulkDelete.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
refetch();
reset();
}
};
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
params.activeTab === CategoryPageTab.categories params.activeTab === CategoryPageTab.categories
? maybe(() => data.category.children.pageInfo) ? maybe(() => data.category.children.pageInfo)
@ -166,191 +170,178 @@ export const CategoryDetails: React.FC<CategoryDetailsProps> = ({
return ( return (
<> <>
<WindowTitle title={maybe(() => data.category.name)} /> <WindowTitle title={maybe(() => data.category.name)} />
<TypedProductBulkDeleteMutation onCompleted={handleBulkProductDelete}> <CategoryUpdatePage
{(productBulkDelete, productBulkDeleteOpts) => ( changeTab={changeTab}
<> currentTab={params.activeTab}
<CategoryUpdatePage category={maybe(() => data.category)}
changeTab={changeTab} disabled={loading}
currentTab={params.activeTab} errors={updateResult.data?.categoryUpdate.errors || []}
category={maybe(() => data.category)} onAddCategory={() => navigate(categoryAddUrl(id))}
disabled={loading} onAddProduct={() => navigate(productAddUrl)}
errors={updateResult.data?.categoryUpdate.errors || []} onBack={() =>
onAddCategory={() => navigate(categoryAddUrl(id))} navigate(
onAddProduct={() => navigate(productAddUrl)} maybe(() => categoryUrl(data.category.parent.id), categoryListUrl())
onBack={() => )
navigate( }
maybe( onCategoryClick={id => () => navigate(categoryUrl(id))}
() => categoryUrl(data.category.parent.id), onDelete={() => openModal("delete")}
categoryListUrl() onImageDelete={() =>
) updateCategory({
) variables: {
id,
input: {
backgroundImage: null
} }
onCategoryClick={id => () => navigate(categoryUrl(id))} }
onDelete={() => openModal("delete")} })
onImageDelete={() => }
updateCategory({ onImageUpload={file =>
variables: { updateCategory({
id, variables: {
input: { id,
backgroundImage: null input: {
} backgroundImage: file
}
})
} }
onImageUpload={file => }
updateCategory({ })
variables: { }
id, onNextPage={loadNextPage}
input: { onPreviousPage={loadPreviousPage}
backgroundImage: file pageInfo={pageInfo}
} onProductClick={id => () => navigate(productUrl(id))}
} onSubmit={formData =>
}) updateCategory({
variables: {
id,
input: {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
}
} }
onNextPage={loadNextPage} }
onPreviousPage={loadPreviousPage} })
pageInfo={pageInfo} }
onProductClick={id => () => navigate(productUrl(id))} products={maybe(() =>
onSubmit={formData => data.category.products.edges.map(edge => edge.node)
updateCategory({
variables: {
id,
input: {
backgroundImageAlt: formData.backgroundImageAlt,
descriptionJson: JSON.stringify(formData.description),
name: formData.name,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
}
}
}
})
}
products={maybe(() =>
data.category.products.edges.map(edge => edge.node)
)}
saveButtonBarState={updateResult.status}
subcategories={maybe(() =>
data.category.children.edges.map(edge => edge.node)
)}
subcategoryListToolbar={
<IconButton
color="primary"
onClick={() =>
openModal("delete-categories", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
productListToolbar={
<IconButton
color="primary"
onClick={() =>
openModal("delete-products", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
<ActionDialog
confirmButtonState={deleteResult.status}
onClose={closeModal}
onConfirm={() => deleteCategory({ variables: { id } })}
open={params.action === "delete"}
title={intl.formatMessage({
defaultMessage: "Delete category",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {categoryName}?"
values={{
categoryName: (
<strong>{maybe(() => data.category.name, "...")}</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also unpin all products assigned to this category, making them unavailable in storefront." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "delete-categories" &&
maybe(() => params.ids.length > 0)
}
confirmButtonState={categoryBulkDeleteOpts.status}
onClose={closeModal}
onConfirm={() =>
categoryBulkDelete({
variables: { ids: params.ids }
}).then(() => refetch())
}
title={intl.formatMessage({
defaultMessage: "Delete categories",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this category?} other{Are you sure you want to delete {displayQuantity} categories?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "delete-products"}
confirmButtonState={productBulkDeleteOpts.status}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
}).then(() => refetch())
}
title={intl.formatMessage({
defaultMessage: "Delete products",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
)} )}
</TypedProductBulkDeleteMutation> saveButtonBarState={updateResult.status}
subcategories={maybe(() =>
data.category.children.edges.map(edge => edge.node)
)}
subcategoryListToolbar={
<IconButton
color="primary"
onClick={() =>
openModal("delete-categories", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
productListToolbar={
<IconButton
color="primary"
onClick={() =>
openModal("delete-products", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
/>
<ActionDialog
confirmButtonState={deleteResult.status}
onClose={closeModal}
onConfirm={() => deleteCategory({ variables: { id } })}
open={params.action === "delete"}
title={intl.formatMessage({
defaultMessage: "Delete category",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {categoryName}?"
values={{
categoryName: (
<strong>{maybe(() => data.category.name, "...")}</strong>
)
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also unpin all products assigned to this category, making them unavailable in storefront." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={
params.action === "delete-categories" &&
maybe(() => params.ids.length > 0)
}
confirmButtonState={categoryBulkDeleteOpts.status}
onClose={closeModal}
onConfirm={() =>
categoryBulkDelete({
variables: { ids: params.ids }
}).then(() => refetch())
}
title={intl.formatMessage({
defaultMessage: "Delete categories",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this category?} other{Are you sure you want to delete {displayQuantity} categories?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
<DialogContentText>
<FormattedMessage defaultMessage="Remember this will also delete all products assigned to this category." />
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "delete-products"}
confirmButtonState={productBulkDeleteOpts.status}
onClose={closeModal}
onConfirm={() =>
productBulkDelete({
variables: { ids: params.ids }
}).then(() => refetch())
}
title={intl.formatMessage({
defaultMessage: "Delete products",
description: "dialog title"
})}
variant="delete"
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
</> </>
); );
}; };

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { TypedMutationInnerProps } from "../../mutations"; import { TypedMutationInnerProps } from "../../mutations";
import { TypedProductImagesReorder } from "../mutations"; import { useProductImagesReorder } from "../mutations";
import { import {
ProductImageReorder, ProductImageReorder,
ProductImageReorderVariables ProductImageReorderVariables
@ -24,10 +24,12 @@ const ProductImagesReorderProvider: React.FC<ProductImagesReorderProviderProps>
productId, productId,
productImages, productImages,
...mutationProps ...mutationProps
}) => ( }) => {
<TypedProductImagesReorder {...mutationProps}> const [mutate, mutationResult] = useProductImagesReorder(mutationProps);
{(mutate, mutationResult) =>
children(opts => { return (
<>
{children(opts => {
const productImagesMap = productImages.reduce((prev, curr) => { const productImagesMap = productImages.reduce((prev, curr) => {
prev[curr.id] = curr; prev[curr.id] = curr;
return prev; return prev;
@ -52,9 +54,9 @@ const ProductImagesReorderProvider: React.FC<ProductImagesReorderProviderProps>
...opts, ...opts,
optimisticResponse optimisticResponse
}); });
}, mutationResult) }, mutationResult)}
} </>
</TypedProductImagesReorder> );
); };
export default ProductImagesReorderProvider; export default ProductImagesReorderProvider;

View file

@ -1,153 +0,0 @@
import React from "react";
import { getMutationProviderData, maybe } from "../../misc";
import { PartialMutationProviderOutput } from "../../types";
import {
TypedProductDeleteMutation,
TypedProductImageCreateMutation,
TypedProductImageDeleteMutation,
TypedProductUpdateMutation,
TypedProductVariantBulkDeleteMutation,
TypedSimpleProductUpdateMutation
} from "../mutations";
import { ProductDelete, ProductDeleteVariables } from "../types/ProductDelete";
import { ProductDetails_product } from "../types/ProductDetails";
import {
ProductImageCreate,
ProductImageCreateVariables
} from "../types/ProductImageCreate";
import {
ProductImageDelete,
ProductImageDeleteVariables
} from "../types/ProductImageDelete";
import {
ProductImageReorder,
ProductImageReorderVariables
} from "../types/ProductImageReorder";
import { ProductUpdate, ProductUpdateVariables } from "../types/ProductUpdate";
import {
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
} from "../types/ProductVariantBulkDelete";
import {
SimpleProductUpdate,
SimpleProductUpdateVariables
} from "../types/SimpleProductUpdate";
import ProductImagesReorderProvider from "./ProductImagesReorder";
interface ProductUpdateOperationsProps {
product: ProductDetails_product;
children: (props: {
bulkProductVariantDelete: PartialMutationProviderOutput<
ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables
>;
createProductImage: PartialMutationProviderOutput<
ProductImageCreate,
ProductImageCreateVariables
>;
deleteProduct: PartialMutationProviderOutput<
ProductDelete,
ProductDeleteVariables
>;
deleteProductImage: PartialMutationProviderOutput<
ProductImageDelete,
ProductImageDeleteVariables
>;
reorderProductImages: PartialMutationProviderOutput<
ProductImageReorder,
ProductImageReorderVariables
>;
updateProduct: PartialMutationProviderOutput<
ProductUpdate,
ProductUpdateVariables
>;
updateSimpleProduct: PartialMutationProviderOutput<
SimpleProductUpdate,
SimpleProductUpdateVariables
>;
}) => React.ReactNode;
onBulkProductVariantDelete?: (data: ProductVariantBulkDelete) => void;
onDelete?: (data: ProductDelete) => void;
onImageCreate?: (data: ProductImageCreate) => void;
onImageDelete?: (data: ProductImageDelete) => void;
onImageReorder?: (data: ProductImageReorder) => void;
onUpdate?: (data: ProductUpdate) => void;
}
const ProductUpdateOperations: React.FC<ProductUpdateOperationsProps> = ({
product,
children,
onBulkProductVariantDelete,
onDelete,
onImageDelete,
onImageCreate,
onImageReorder,
onUpdate
}) => {
const productId = product ? product.id : "";
return (
<TypedProductUpdateMutation onCompleted={onUpdate}>
{(...updateProduct) => (
<ProductImagesReorderProvider
productId={productId}
productImages={maybe(() => product.images, [])}
onCompleted={onImageReorder}
>
{(...reorderProductImages) => (
<TypedProductImageCreateMutation onCompleted={onImageCreate}>
{(...createProductImage) => (
<TypedProductDeleteMutation onCompleted={onDelete}>
{(...deleteProduct) => (
<TypedProductImageDeleteMutation
onCompleted={onImageDelete}
>
{(...deleteProductImage) => (
<TypedSimpleProductUpdateMutation
onCompleted={onUpdate}
>
{(...updateSimpleProduct) => (
<TypedProductVariantBulkDeleteMutation
onCompleted={onBulkProductVariantDelete}
>
{(...bulkProductVariantDelete) =>
children({
bulkProductVariantDelete: getMutationProviderData(
...bulkProductVariantDelete
),
createProductImage: getMutationProviderData(
...createProductImage
),
deleteProduct: getMutationProviderData(
...deleteProduct
),
deleteProductImage: getMutationProviderData(
...deleteProductImage
),
reorderProductImages: getMutationProviderData(
...reorderProductImages
),
updateProduct: getMutationProviderData(
...updateProduct
),
updateSimpleProduct: getMutationProviderData(
...updateSimpleProduct
)
})
}
</TypedProductVariantBulkDeleteMutation>
)}
</TypedSimpleProductUpdateMutation>
)}
</TypedProductImageDeleteMutation>
)}
</TypedProductDeleteMutation>
)}
</TypedProductImageCreateMutation>
)}
</ProductImagesReorderProvider>
)}
</TypedProductUpdateMutation>
);
};
export default ProductUpdateOperations;

View file

@ -1,77 +0,0 @@
import React from "react";
import { getMutationProviderData } from "../../misc";
import { PartialMutationProviderOutput } from "../../types";
import {
TypedVariantDeleteMutation,
TypedVariantImageAssignMutation,
TypedVariantImageUnassignMutation,
TypedVariantUpdateMutation
} from "../mutations";
import { VariantDelete, VariantDeleteVariables } from "../types/VariantDelete";
import {
VariantImageAssign,
VariantImageAssignVariables
} from "../types/VariantImageAssign";
import {
VariantImageUnassign,
VariantImageUnassignVariables
} from "../types/VariantImageUnassign";
import { VariantUpdate, VariantUpdateVariables } from "../types/VariantUpdate";
interface VariantDeleteOperationsProps {
children: (props: {
deleteVariant: PartialMutationProviderOutput<
VariantDelete,
VariantDeleteVariables
>;
updateVariant: PartialMutationProviderOutput<
VariantUpdate,
VariantUpdateVariables
>;
assignImage: PartialMutationProviderOutput<
VariantImageAssign,
VariantImageAssignVariables
>;
unassignImage: PartialMutationProviderOutput<
VariantImageUnassign,
VariantImageUnassignVariables
>;
}) => React.ReactNode;
onDelete?: (data: VariantDelete) => void;
onImageAssign?: (data: VariantImageAssign) => void;
onImageUnassign?: (data: VariantImageUnassign) => void;
onUpdate?: (data: VariantUpdate) => void;
}
const VariantUpdateOperations: React.FC<VariantDeleteOperationsProps> = ({
children,
onDelete,
onUpdate,
onImageAssign,
onImageUnassign
}) => (
<TypedVariantImageAssignMutation onCompleted={onImageAssign}>
{(...assignImage) => (
<TypedVariantImageUnassignMutation onCompleted={onImageUnassign}>
{(...unassignImage) => (
<TypedVariantUpdateMutation onCompleted={onUpdate}>
{(...updateVariant) => (
<TypedVariantDeleteMutation onCompleted={onDelete}>
{(...deleteVariant) =>
children({
assignImage: getMutationProviderData(...assignImage),
deleteVariant: getMutationProviderData(...deleteVariant),
unassignImage: getMutationProviderData(...unassignImage),
updateVariant: getMutationProviderData(...updateVariant)
})
}
</TypedVariantDeleteMutation>
)}
</TypedVariantUpdateMutation>
)}
</TypedVariantImageUnassignMutation>
)}
</TypedVariantImageAssignMutation>
);
export default VariantUpdateOperations;

View file

@ -13,7 +13,6 @@ import {
import makeMutation from "@saleor/hooks/makeMutation"; import makeMutation from "@saleor/hooks/makeMutation";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedMutation } from "../mutations";
import { import {
productBulkDelete, productBulkDelete,
productBulkDeleteVariables productBulkDeleteVariables
@ -80,7 +79,7 @@ export const productImageCreateMutation = gql`
} }
} }
`; `;
export const TypedProductImageCreateMutation = TypedMutation< export const useProductImageCreateMutation = makeMutation<
ProductImageCreate, ProductImageCreate,
ProductImageCreateVariables ProductImageCreateVariables
>(productImageCreateMutation); >(productImageCreateMutation);
@ -98,7 +97,7 @@ export const productDeleteMutation = gql`
} }
} }
`; `;
export const TypedProductDeleteMutation = TypedMutation< export const useProductDeleteMutation = makeMutation<
ProductDelete, ProductDelete,
ProductDeleteVariables ProductDeleteVariables
>(productDeleteMutation); >(productDeleteMutation);
@ -122,7 +121,7 @@ export const productImagesReorder = gql`
} }
} }
`; `;
export const TypedProductImagesReorder = TypedMutation< export const useProductImagesReorder = makeMutation<
ProductImageReorder, ProductImageReorder,
ProductImageReorderVariables ProductImageReorderVariables
>(productImagesReorder); >(productImagesReorder);
@ -167,7 +166,7 @@ export const productUpdateMutation = gql`
} }
} }
`; `;
export const TypedProductUpdateMutation = TypedMutation< export const useProductUpdateMutation = makeMutation<
ProductUpdate, ProductUpdate,
ProductUpdateVariables ProductUpdateVariables
>(productUpdateMutation); >(productUpdateMutation);
@ -263,7 +262,7 @@ export const simpleProductUpdateMutation = gql`
} }
} }
`; `;
export const TypedSimpleProductUpdateMutation = TypedMutation< export const useSimpleProductUpdateMutation = makeMutation<
SimpleProductUpdate, SimpleProductUpdate,
SimpleProductUpdateVariables SimpleProductUpdateVariables
>(simpleProductUpdateMutation); >(simpleProductUpdateMutation);
@ -316,7 +315,7 @@ export const productCreateMutation = gql`
} }
} }
`; `;
export const TypedProductCreateMutation = TypedMutation< export const useProductCreateMutation = makeMutation<
ProductCreate, ProductCreate,
ProductCreateVariables ProductCreateVariables
>(productCreateMutation); >(productCreateMutation);
@ -334,7 +333,7 @@ export const variantDeleteMutation = gql`
} }
} }
`; `;
export const TypedVariantDeleteMutation = TypedMutation< export const useVariantDeleteMutation = makeMutation<
VariantDelete, VariantDelete,
VariantDeleteVariables VariantDeleteVariables
>(variantDeleteMutation); >(variantDeleteMutation);
@ -406,7 +405,7 @@ export const variantUpdateMutation = gql`
} }
} }
`; `;
export const TypedVariantUpdateMutation = TypedMutation< export const useVariantUpdateMutation = makeMutation<
VariantUpdate, VariantUpdate,
VariantUpdateVariables VariantUpdateVariables
>(variantUpdateMutation); >(variantUpdateMutation);
@ -425,7 +424,7 @@ export const variantCreateMutation = gql`
} }
} }
`; `;
export const TypedVariantCreateMutation = TypedMutation< export const useVariantCreateMutation = makeMutation<
VariantCreate, VariantCreate,
VariantCreateVariables VariantCreateVariables
>(variantCreateMutation); >(variantCreateMutation);
@ -446,7 +445,7 @@ export const productImageDeleteMutation = gql`
} }
} }
`; `;
export const TypedProductImageDeleteMutation = TypedMutation< export const useProductImageDeleteMutation = makeMutation<
ProductImageDelete, ProductImageDelete,
ProductImageDeleteVariables ProductImageDeleteVariables
>(productImageDeleteMutation); >(productImageDeleteMutation);
@ -465,7 +464,7 @@ export const productImageUpdateMutation = gql`
} }
} }
`; `;
export const TypedProductImageUpdateMutation = TypedMutation< export const useProductImageUpdateMutation = makeMutation<
ProductImageUpdate, ProductImageUpdate,
ProductImageUpdateVariables ProductImageUpdateVariables
>(productImageUpdateMutation); >(productImageUpdateMutation);
@ -484,7 +483,7 @@ export const variantImageAssignMutation = gql`
} }
} }
`; `;
export const TypedVariantImageAssignMutation = TypedMutation< export const useVariantImageAssignMutation = makeMutation<
VariantImageAssign, VariantImageAssign,
VariantImageAssignVariables VariantImageAssignVariables
>(variantImageAssignMutation); >(variantImageAssignMutation);
@ -503,7 +502,7 @@ export const variantImageUnassignMutation = gql`
} }
} }
`; `;
export const TypedVariantImageUnassignMutation = TypedMutation< export const useVariantImageUnassignMutation = makeMutation<
VariantImageUnassign, VariantImageUnassign,
VariantImageUnassignVariables VariantImageUnassignVariables
>(variantImageUnassignMutation); >(variantImageUnassignMutation);
@ -518,7 +517,7 @@ export const productBulkDeleteMutation = gql`
} }
} }
`; `;
export const TypedProductBulkDeleteMutation = TypedMutation< export const useProductBulkDeleteMutation = makeMutation<
productBulkDelete, productBulkDelete,
productBulkDeleteVariables productBulkDeleteVariables
>(productBulkDeleteMutation); >(productBulkDeleteMutation);
@ -533,7 +532,7 @@ export const productBulkPublishMutation = gql`
} }
} }
`; `;
export const TypedProductBulkPublishMutation = TypedMutation< export const useProductBulkPublishMutation = makeMutation<
productBulkPublish, productBulkPublish,
productBulkPublishVariables productBulkPublishVariables
>(productBulkPublishMutation); >(productBulkPublishMutation);
@ -566,7 +565,7 @@ export const ProductVariantBulkDeleteMutation = gql`
} }
} }
`; `;
export const TypedProductVariantBulkDeleteMutation = TypedMutation< export const useProductVariantBulkDeleteMutation = makeMutation<
ProductVariantBulkDelete, ProductVariantBulkDelete,
ProductVariantBulkDeleteVariables ProductVariantBulkDeleteVariables
>(ProductVariantBulkDeleteMutation); >(ProductVariantBulkDeleteMutation);

View file

@ -10,7 +10,6 @@ import { warehouseFragment } from "@saleor/fragments/warehouses";
import makeQuery from "@saleor/hooks/makeQuery"; import makeQuery from "@saleor/hooks/makeQuery";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { TypedQuery } from "../queries";
import { CountAllProducts } from "./types/CountAllProducts"; import { CountAllProducts } from "./types/CountAllProducts";
import { import {
CreateMultipleVariantsData, CreateMultipleVariantsData,
@ -150,10 +149,9 @@ const productListQuery = gql`
} }
} }
`; `;
export const TypedProductListQuery = TypedQuery< export const useProductListQuery = makeQuery<ProductList, ProductListVariables>(
ProductList, productListQuery
ProductListVariables );
>(productListQuery);
const countAllProductsQuery = gql` const countAllProductsQuery = gql`
query CountAllProducts { query CountAllProducts {
@ -174,7 +172,7 @@ const productDetailsQuery = gql`
} }
} }
`; `;
export const TypedProductDetailsQuery = TypedQuery< export const useProductDetails = makeQuery<
ProductDetails, ProductDetails,
ProductDetailsVariables ProductDetailsVariables
>(productDetailsQuery); >(productDetailsQuery);
@ -187,7 +185,7 @@ const productVariantQuery = gql`
} }
} }
`; `;
export const TypedProductVariantQuery = TypedQuery< export const useProductVariantQuery = makeQuery<
ProductVariantDetails, ProductVariantDetails,
ProductVariantDetailsVariables ProductVariantDetailsVariables
>(productVariantQuery); >(productVariantQuery);
@ -231,7 +229,7 @@ const productVariantCreateQuery = gql`
} }
} }
`; `;
export const TypedProductVariantCreateQuery = TypedQuery< export const useProductVariantCreateQuery = makeQuery<
ProductVariantCreateData, ProductVariantCreateData,
ProductVariantCreateDataVariables ProductVariantCreateDataVariables
>(productVariantCreateQuery); >(productVariantCreateQuery);
@ -253,7 +251,7 @@ const productImageQuery = gql`
} }
} }
`; `;
export const TypedProductImageQuery = TypedQuery< export const useProductImageQuery = makeQuery<
ProductImageById, ProductImageById,
ProductImageByIdVariables ProductImageByIdVariables
>(productImageQuery); >(productImageQuery);
@ -288,7 +286,7 @@ const availableInGridAttributes = gql`
} }
} }
`; `;
export const AvailableInGridAttributesQuery = TypedQuery< export const useAvailableInGridAttributesQuery = makeQuery<
GridAttributes, GridAttributes,
GridAttributesVariables GridAttributesVariables
>(availableInGridAttributes); >(availableInGridAttributes);

View file

@ -10,12 +10,11 @@ import { useWarehouseList } from "@saleor/warehouses/queries";
import React from "react"; import React from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { decimal, maybe, weight } from "../../misc"; import { decimal, weight } from "../../misc";
import ProductCreatePage, { import ProductCreatePage, {
ProductCreatePageSubmitData ProductCreatePageSubmitData
} from "../components/ProductCreatePage"; } from "../components/ProductCreatePage";
import { TypedProductCreateMutation } from "../mutations"; import { useProductCreateMutation } from "../mutations";
import { ProductCreate } from "../types/ProductCreate";
import { productListUrl, productUrl } from "../urls"; import { productListUrl, productUrl } from "../urls";
export const ProductCreateView: React.FC = () => { export const ProductCreateView: React.FC = () => {
@ -53,118 +52,104 @@ export const ProductCreateView: React.FC = () => {
const handleBack = () => navigate(productListUrl()); const handleBack = () => navigate(productListUrl());
const handleSuccess = (data: ProductCreate) => { const [productCreate, productCreateOpts] = useProductCreateMutation({
if (data.productCreate.errors.length === 0) { onCompleted: data => {
notify({ if (data.productCreate.errors.length === 0) {
status: "success", notify({
text: intl.formatMessage({ status: "success",
defaultMessage: "Product created" text: intl.formatMessage({
}) defaultMessage: "Product created"
}); })
navigate(productUrl(data.productCreate.product.id)); });
navigate(productUrl(data.productCreate.product.id));
}
} }
});
const handleSubmit = (formData: ProductCreatePageSubmitData) => {
productCreate({
variables: {
attributes: formData.attributes.map(attribute => ({
id: attribute.id,
values: attribute.value
})),
basePrice: decimal(formData.basePrice),
category: formData.category,
chargeTaxes: formData.chargeTaxes,
collections: formData.collections,
descriptionJson: JSON.stringify(formData.description),
isPublished: formData.isPublished,
name: formData.name,
productType: formData.productType,
publicationDate:
formData.publicationDate !== "" ? formData.publicationDate : null,
seo: {
description: formData.seoDescription,
title: formData.seoTitle
},
sku: formData.sku,
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
})),
trackInventory: formData.trackInventory,
weight: weight(formData.weight)
}
});
}; };
return ( return (
<TypedProductCreateMutation onCompleted={handleSuccess}> <>
{(productCreate, productCreateOpts) => { <WindowTitle
const handleSubmit = (formData: ProductCreatePageSubmitData) => { title={intl.formatMessage({
productCreate({ defaultMessage: "Create Product",
variables: { description: "window title"
attributes: formData.attributes.map(attribute => ({ })}
id: attribute.id, />
values: attribute.value <ProductCreatePage
})), currency={shop?.defaultCurrency}
basePrice: decimal(formData.basePrice), categories={(searchCategoryOpts.data?.search.edges || []).map(
category: formData.category, edge => edge.node
chargeTaxes: formData.chargeTaxes, )}
collections: formData.collections, collections={(searchCollectionOpts.data?.search.edges || []).map(
descriptionJson: JSON.stringify(formData.description), edge => edge.node
isPublished: formData.isPublished, )}
name: formData.name, disabled={productCreateOpts.loading}
productType: formData.productType, errors={productCreateOpts.data?.productCreate.errors || []}
publicationDate: fetchCategories={searchCategory}
formData.publicationDate !== "" fetchCollections={searchCollection}
? formData.publicationDate fetchProductTypes={searchProductTypes}
: null, header={intl.formatMessage({
seo: { defaultMessage: "New Product",
description: formData.seoDescription, description: "page header"
title: formData.seoTitle })}
}, productTypes={searchProductTypesOpts.data?.search.edges.map(
sku: formData.sku, edge => edge.node
stocks: formData.stocks.map(stock => ({ )}
quantity: parseInt(stock.value, 0), onBack={handleBack}
warehouse: stock.id onSubmit={handleSubmit}
})), saveButtonBarState={productCreateOpts.status}
trackInventory: formData.trackInventory, fetchMoreCategories={{
weight: weight(formData.weight) hasMore: searchCategoryOpts.data?.search.pageInfo.hasNextPage,
} loading: searchCategoryOpts.loading,
}); onFetchMore: loadMoreCategories
}; }}
fetchMoreCollections={{
return ( hasMore: searchCollectionOpts.data?.search.pageInfo.hasNextPage,
<> loading: searchCollectionOpts.loading,
<WindowTitle onFetchMore: loadMoreCollections
title={intl.formatMessage({ }}
defaultMessage: "Create Product", fetchMoreProductTypes={{
description: "window title" hasMore: searchProductTypesOpts.data?.search.pageInfo.hasNextPage,
})} loading: searchProductTypesOpts.loading,
/> onFetchMore: loadMoreProductTypes
<ProductCreatePage }}
currency={maybe(() => shop.defaultCurrency)} warehouses={
categories={maybe( warehouses.data?.warehouses.edges.map(edge => edge.node) || []
() => searchCategoryOpts.data.search.edges, }
[] weightUnit={shop?.defaultWeightUnit}
).map(edge => edge.node)} />
collections={maybe( </>
() => searchCollectionOpts.data.search.edges,
[]
).map(edge => edge.node)}
disabled={productCreateOpts.loading}
errors={productCreateOpts.data?.productCreate.errors || []}
fetchCategories={searchCategory}
fetchCollections={searchCollection}
fetchProductTypes={searchProductTypes}
header={intl.formatMessage({
defaultMessage: "New Product",
description: "page header"
})}
productTypes={maybe(() =>
searchProductTypesOpts.data.search.edges.map(edge => edge.node)
)}
onBack={handleBack}
onSubmit={handleSubmit}
saveButtonBarState={productCreateOpts.status}
fetchMoreCategories={{
hasMore: maybe(
() => searchCategoryOpts.data.search.pageInfo.hasNextPage
),
loading: searchCategoryOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() => searchCollectionOpts.data.search.pageInfo.hasNextPage
),
loading: searchCollectionOpts.loading,
onFetchMore: loadMoreCollections
}}
fetchMoreProductTypes={{
hasMore: maybe(
() => searchProductTypesOpts.data.search.pageInfo.hasNextPage
),
loading: searchProductTypesOpts.loading,
onFetchMore: loadMoreProductTypes
}}
warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
}
weightUnit={shop?.defaultWeightUnit}
/>
</>
);
}}
</TypedProductCreateMutation>
); );
}; };
export default ProductCreateView; export default ProductCreateView;

View file

@ -7,14 +7,12 @@ import { commonMessages } from "@saleor/intl";
import React from "react"; import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { maybe } from "../../misc";
import ProductImagePage from "../components/ProductImagePage"; import ProductImagePage from "../components/ProductImagePage";
import { import {
TypedProductImageDeleteMutation, useProductImageDeleteMutation,
TypedProductImageUpdateMutation useProductImageUpdateMutation
} from "../mutations"; } from "../mutations";
import { TypedProductImageQuery } from "../queries"; import { useProductImageQuery } from "../queries";
import { ProductImageUpdate } from "../types/ProductImageUpdate";
import { import {
productImageUrl, productImageUrl,
ProductImageUrlQueryParams, ProductImageUrlQueryParams,
@ -38,93 +36,84 @@ export const ProductImage: React.FC<ProductImageProps> = ({
const intl = useIntl(); const intl = useIntl();
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
const handleUpdateSuccess = (data: ProductImageUpdate) => {
if (data.productImageUpdate.errors.length === 0) { const { data, loading } = useProductImageQuery({
notify({ displayLoader: true,
status: "success", variables: {
text: intl.formatMessage(commonMessages.savedChanges) imageId,
}); productId
} }
});
const [updateImage, updateResult] = useProductImageUpdateMutation({
onCompleted: data => {
if (data.productImageUpdate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
}
}
});
const [deleteImage, deleteResult] = useProductImageDeleteMutation({
onCompleted: handleBack
});
const product = data?.product;
if (product === null) {
return <NotFoundPage onBack={() => navigate(productListUrl())} />;
}
const handleDelete = () => deleteImage({ variables: { id: imageId } });
const handleImageClick = (id: string) => () =>
navigate(productImageUrl(productId, id));
const handleUpdate = (formData: { description: string }) => {
updateImage({
variables: {
alt: formData.description,
id: imageId
}
});
}; };
const image = data?.product?.mainImage;
return ( return (
<TypedProductImageQuery <>
displayLoader <ProductImagePage
variables={{ disabled={loading}
imageId, product={data?.product?.name}
productId image={image || null}
}} images={data?.product?.images}
> onBack={handleBack}
{({ data, loading }) => { onDelete={() =>
const product = data?.product; navigate(
productImageUrl(productId, imageId, {
if (product === null) { action: "remove"
return <NotFoundPage onBack={() => navigate(productListUrl())} />; })
)
} }
onRowClick={handleImageClick}
return ( onSubmit={handleUpdate}
<TypedProductImageUpdateMutation onCompleted={handleUpdateSuccess}> saveButtonBarState={updateResult.status}
{(updateImage, updateResult) => ( />
<TypedProductImageDeleteMutation onCompleted={handleBack}> <ActionDialog
{(deleteImage, deleteResult) => { onClose={() => navigate(productImageUrl(productId, imageId), true)}
const handleDelete = () => onConfirm={handleDelete}
deleteImage({ variables: { id: imageId } }); open={params.action === "remove"}
const handleImageClick = (id: string) => () => title={intl.formatMessage({
navigate(productImageUrl(productId, id)); defaultMessage: "Delete Image",
const handleUpdate = (formData: { description: string }) => { description: "dialog header"
updateImage({ })}
variables: { variant="delete"
alt: formData.description, confirmButtonState={deleteResult.status}
id: imageId >
} <DialogContentText>
}); <FormattedMessage defaultMessage="Are you sure you want to delete this image?" />
}; </DialogContentText>
const image = data && data.product && data.product.mainImage; </ActionDialog>
</>
return (
<>
<ProductImagePage
disabled={loading}
product={maybe(() => data.product.name)}
image={image || null}
images={maybe(() => data.product.images)}
onBack={handleBack}
onDelete={() =>
navigate(
productImageUrl(productId, imageId, {
action: "remove"
})
)
}
onRowClick={handleImageClick}
onSubmit={handleUpdate}
saveButtonBarState={updateResult.status}
/>
<ActionDialog
onClose={() =>
navigate(productImageUrl(productId, imageId), true)
}
onConfirm={handleDelete}
open={params.action === "remove"}
title={intl.formatMessage({
defaultMessage: "Delete Image",
description: "dialog header"
})}
variant="delete"
confirmButtonState={deleteResult.status}
>
<DialogContentText>
<FormattedMessage defaultMessage="Are you sure you want to delete this image?" />
</DialogContentText>
</ActionDialog>
</>
);
}}
</TypedProductImageDeleteMutation>
)}
</TypedProductImageUpdateMutation>
);
}}
</TypedProductImageQuery>
); );
}; };
export default ProductImage; export default ProductImage;

View file

@ -45,18 +45,16 @@ import { FormattedMessage, useIntl } from "react-intl";
import ProductListPage from "../../components/ProductListPage"; import ProductListPage from "../../components/ProductListPage";
import { import {
TypedProductBulkDeleteMutation, useProductBulkDeleteMutation,
TypedProductBulkPublishMutation, useProductBulkPublishMutation,
useProductExport useProductExport
} from "../../mutations"; } from "../../mutations";
import { import {
AvailableInGridAttributesQuery, useAvailableInGridAttributesQuery,
TypedProductListQuery,
useCountAllProducts, useCountAllProducts,
useInitialProductFilterDataQuery useInitialProductFilterDataQuery,
useProductListQuery
} from "../../queries"; } from "../../queries";
import { productBulkDelete } from "../../types/productBulkDelete";
import { productBulkPublish } from "../../types/productBulkPublish";
import { import {
productAddUrl, productAddUrl,
productListUrl, productListUrl,
@ -235,6 +233,53 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
}), }),
[params, settings.rowNumber] [params, settings.rowNumber]
); );
const { data, loading, refetch } = useProductListQuery({
displayLoader: true,
variables: queryVariables
});
function filterColumnIds(columns: ProductListColumns[]) {
return columns
.filter(isAttributeColumnValue)
.map(getAttributeIdFromColumnValue);
}
const attributes = useAvailableInGridAttributesQuery({
variables: { first: 6, ids: filterColumnIds(settings.columns) }
});
const [
productBulkDelete,
productBulkDeleteOpts
] = useProductBulkDeleteMutation({
onCompleted: data => {
if (data.productBulkDelete.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
reset();
refetch();
}
}
});
const [
productBulkPublish,
productBulkPublishOpts
] = useProductBulkPublishMutation({
onCompleted: data => {
if (data.productBulkPublish.errors.length === 0) {
closeModal();
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
reset();
refetch();
}
}
});
const filterOpts = getFilterOpts( const filterOpts = getFilterOpts(
params, params,
@ -262,345 +307,259 @@ export const ProductList: React.FC<ProductListProps> = ({ params }) => {
} }
); );
function filterColumnIds(columns: ProductListColumns[]) { const { loadNextPage, loadPreviousPage, pageInfo } = paginate(
return columns maybe(() => data.products.pageInfo),
.filter(isAttributeColumnValue) paginationState,
.map(getAttributeIdFromColumnValue); params
} );
return ( return (
<AvailableInGridAttributesQuery <>
variables={{ first: 6, ids: filterColumnIds(settings.columns) }} <ProductListPage
> activeAttributeSortId={params.attributeId}
{attributes => ( sort={{
<TypedProductListQuery displayLoader variables={queryVariables}> asc: params.asc,
{({ data, loading, refetch }) => { sort: params.sort
const { loadNextPage, loadPreviousPage, pageInfo } = paginate( }}
maybe(() => data.products.pageInfo), onSort={handleSort}
paginationState, availableInGridAttributes={maybe(
params () => attributes.data.availableInGrid.edges.map(edge => edge.node),
); []
)}
const handleBulkDelete = (data: productBulkDelete) => { currencySymbol={currencySymbol}
if (data.productBulkDelete.errors.length === 0) { currentTab={currentTab}
closeModal(); defaultSettings={defaultListSettings[ListViews.PRODUCT_LIST]}
notify({ filterOpts={filterOpts}
status: "success", gridAttributes={maybe(
text: intl.formatMessage(commonMessages.savedChanges) () => attributes.data.grid.edges.map(edge => edge.node),
}); []
reset(); )}
refetch(); totalGridAttributes={maybe(
() => attributes.data.availableInGrid.totalCount,
0
)}
settings={settings}
loading={attributes.loading}
hasMore={maybe(
() => attributes.data.availableInGrid.pageInfo.hasNextPage,
false
)}
onAdd={() => navigate(productAddUrl)}
disabled={loading}
products={maybe(() => data.products.edges.map(edge => edge.node))}
onFetchMore={() =>
attributes.loadMore(
(prev, next) => {
if (
prev.availableInGrid.pageInfo.endCursor ===
next.availableInGrid.pageInfo.endCursor
) {
return prev;
} }
}; return {
...prev,
const handleBulkPublish = (data: productBulkPublish) => { availableInGrid: {
if (data.productBulkPublish.errors.length === 0) { ...prev.availableInGrid,
closeModal(); edges: [
notify({ ...prev.availableInGrid.edges,
status: "success", ...next.availableInGrid.edges
text: intl.formatMessage(commonMessages.savedChanges) ],
}); pageInfo: next.availableInGrid.pageInfo
reset(); }
refetch(); };
},
{
after: attributes.data.availableInGrid.pageInfo.endCursor
}
)
}
onNextPage={loadNextPage}
onPreviousPage={loadPreviousPage}
onUpdateListSettings={updateListSettings}
pageInfo={pageInfo}
onRowClick={id => () => navigate(productUrl(id))}
onAll={resetFilters}
toolbar={
<>
<Button
color="primary"
onClick={() =>
openModal("unpublish", {
ids: listElements
})
} }
}; >
<FormattedMessage
return ( defaultMessage="Unpublish"
<TypedProductBulkDeleteMutation onCompleted={handleBulkDelete}> description="unpublish product, button"
{(productBulkDelete, productBulkDeleteOpts) => ( />
<TypedProductBulkPublishMutation </Button>
onCompleted={handleBulkPublish} <Button
> color="primary"
{(productBulkPublish, productBulkPublishOpts) => ( onClick={() =>
<> openModal("publish", {
<ProductListPage ids: listElements
activeAttributeSortId={params.attributeId} })
sort={{ }
asc: params.asc, >
sort: params.sort <FormattedMessage
}} defaultMessage="Publish"
onSort={handleSort} description="publish product, button"
availableInGridAttributes={maybe( />
() => </Button>
attributes.data.availableInGrid.edges.map( <IconButton
edge => edge.node color="primary"
), onClick={() =>
[] openModal("delete", {
)} ids: listElements
currencySymbol={currencySymbol} })
currentTab={currentTab} }
defaultSettings={ >
defaultListSettings[ListViews.PRODUCT_LIST] <DeleteIcon />
} </IconButton>
filterOpts={filterOpts} </>
gridAttributes={maybe( }
() => isChecked={isSelected}
attributes.data.grid.edges.map(edge => edge.node), selected={listElements.length}
[] toggle={toggle}
)} toggleAll={toggleAll}
totalGridAttributes={maybe( onSearchChange={handleSearchChange}
() => attributes.data.availableInGrid.totalCount, onFilterChange={changeFilters}
0 onTabSave={() => openModal("save-search")}
)} onTabDelete={() => openModal("delete-search")}
settings={settings} onTabChange={handleTabChange}
loading={attributes.loading} initialSearch={params.query || ""}
hasMore={maybe( tabs={getFilterTabs().map(tab => tab.name)}
() => onExport={() => openModal("export")}
attributes.data.availableInGrid.pageInfo />
.hasNextPage, <ActionDialog
false open={params.action === "delete"}
)} confirmButtonState={productBulkDeleteOpts.status}
onAdd={() => navigate(productAddUrl)} onClose={closeModal}
disabled={loading} onConfirm={() =>
products={maybe(() => productBulkDelete({
data.products.edges.map(edge => edge.node) variables: { ids: params.ids }
)} })
onFetchMore={() => }
attributes.loadMore( title={intl.formatMessage({
(prev, next) => { defaultMessage: "Delete Products",
if ( description: "dialog header"
prev.availableInGrid.pageInfo.endCursor === })}
next.availableInGrid.pageInfo.endCursor variant="delete"
) { >
return prev; <DialogContentText>
} <FormattedMessage
return { defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}"
...prev, description="dialog content"
availableInGrid: { values={{
...prev.availableInGrid, counter: maybe(() => params.ids.length),
edges: [ displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
...prev.availableInGrid.edges, }}
...next.availableInGrid.edges />
], </DialogContentText>
pageInfo: next.availableInGrid.pageInfo </ActionDialog>
} <ActionDialog
}; open={params.action === "publish"}
}, confirmButtonState={productBulkPublishOpts.status}
{ onClose={closeModal}
after: onConfirm={() =>
attributes.data.availableInGrid.pageInfo productBulkPublish({
.endCursor variables: {
} ids: params.ids,
) isPublished: true
} }
onNextPage={loadNextPage} })
onPreviousPage={loadPreviousPage} }
onUpdateListSettings={updateListSettings} title={intl.formatMessage({
pageInfo={pageInfo} defaultMessage: "Publish Products",
onRowClick={id => () => navigate(productUrl(id))} description: "dialog header"
onAll={resetFilters} })}
toolbar={ >
<> <DialogContentText>
<Button <FormattedMessage
color="primary" defaultMessage="{counter,plural,one{Are you sure you want to publish this product?} other{Are you sure you want to publish {displayQuantity} products?}}"
onClick={() => description="dialog content"
openModal("unpublish", { values={{
ids: listElements counter: maybe(() => params.ids.length),
}) displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
} }}
> />
<FormattedMessage </DialogContentText>
defaultMessage="Unpublish" </ActionDialog>
description="unpublish product, button" <ActionDialog
/> open={params.action === "unpublish"}
</Button> confirmButtonState={productBulkPublishOpts.status}
<Button onClose={closeModal}
color="primary" onConfirm={() =>
onClick={() => productBulkPublish({
openModal("publish", { variables: {
ids: listElements ids: params.ids,
}) isPublished: false
} }
> })
<FormattedMessage }
defaultMessage="Publish" title={intl.formatMessage({
description="publish product, button" defaultMessage: "Unpublish Products",
/> description: "dialog header"
</Button> })}
<IconButton >
color="primary" <DialogContentText>
onClick={() => <FormattedMessage
openModal("delete", { defaultMessage="{counter,plural,one{Are you sure you want to unpublish this product?} other{Are you sure you want to unpublish {displayQuantity} products?}}"
ids: listElements description="dialog content"
}) values={{
} counter: maybe(() => params.ids.length),
> displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
<DeleteIcon /> }}
</IconButton> />
</> </DialogContentText>
} </ActionDialog>
isChecked={isSelected} <ProductExportDialog
selected={listElements.length} attributes={(searchAttributes.result.data?.search.edges || []).map(
toggle={toggle} edge => edge.node
toggleAll={toggleAll} )}
onSearchChange={handleSearchChange} hasMore={searchAttributes.result.data?.search.pageInfo.hasNextPage}
onFilterChange={changeFilters} loading={searchAttributes.result.loading}
onTabSave={() => openModal("save-search")} onFetch={searchAttributes.search}
onTabDelete={() => openModal("delete-search")} onFetchMore={searchAttributes.loadMore}
onTabChange={handleTabChange} open={params.action === "export"}
initialSearch={params.query || ""} confirmButtonState={exportProductsOpts.status}
tabs={getFilterTabs().map(tab => tab.name)} errors={exportProductsOpts.data?.exportProducts.errors || []}
onExport={() => openModal("export")} productQuantity={{
/> all: countAllProducts.data?.products.totalCount,
<ActionDialog filter: data?.products.totalCount
open={params.action === "delete"} }}
confirmButtonState={productBulkDeleteOpts.status} selectedProducts={listElements.length}
onClose={closeModal} warehouses={
onConfirm={() => warehouses.data?.warehouses.edges.map(edge => edge.node) || []
productBulkDelete({ }
variables: { ids: params.ids } onClose={closeModal}
}) onSubmit={data =>
} exportProducts({
title={intl.formatMessage({ variables: {
defaultMessage: "Delete Products", input: {
description: "dialog header" ...data,
})} filter,
variant="delete" ids: listElements
> }
<DialogContentText> }
<FormattedMessage })
defaultMessage="{counter,plural,one{Are you sure you want to delete this product?} other{Are you sure you want to delete {displayQuantity} products?}}" }
description="dialog content" />
values={{ <SaveFilterTabDialog
counter: maybe(() => params.ids.length), open={params.action === "save-search"}
displayQuantity: ( confirmButtonState="default"
<strong> onClose={closeModal}
{maybe(() => params.ids.length)} onSubmit={handleFilterTabSave}
</strong> />
) <DeleteFilterTabDialog
}} open={params.action === "delete-search"}
/> confirmButtonState="default"
</DialogContentText> onClose={closeModal}
</ActionDialog> onSubmit={handleFilterTabDelete}
<ActionDialog tabName={maybe(() => tabs[currentTab - 1].name, "...")}
open={params.action === "publish"} />
confirmButtonState={productBulkPublishOpts.status} </>
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: true
}
})
}
title={intl.formatMessage({
defaultMessage: "Publish Products",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to publish this product?} other{Are you sure you want to publish {displayQuantity} products?}}"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "unpublish"}
confirmButtonState={productBulkPublishOpts.status}
onClose={closeModal}
onConfirm={() =>
productBulkPublish({
variables: {
ids: params.ids,
isPublished: false
}
})
}
title={intl.formatMessage({
defaultMessage: "Unpublish Products",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to unpublish this product?} other{Are you sure you want to unpublish {displayQuantity} products?}}"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>
{maybe(() => params.ids.length)}
</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
<ProductExportDialog
attributes={(
searchAttributes.result.data?.search.edges || []
).map(edge => edge.node)}
hasMore={
searchAttributes.result.data?.search.pageInfo
.hasNextPage
}
loading={searchAttributes.result.loading}
onFetch={searchAttributes.search}
onFetchMore={searchAttributes.loadMore}
open={params.action === "export"}
confirmButtonState={exportProductsOpts.status}
errors={
exportProductsOpts.data?.exportProducts.errors || []
}
productQuantity={{
all: countAllProducts.data?.products.totalCount,
filter: data?.products.totalCount
}}
selectedProducts={listElements.length}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
onClose={closeModal}
onSubmit={data =>
exportProducts({
variables: {
input: {
...data,
filter,
ids: listElements
}
}
})
}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleFilterTabDelete}
tabName={maybe(
() => tabs[currentTab - 1].name,
"..."
)}
/>
</>
)}
</TypedProductBulkPublishMutation>
)}
</TypedProductBulkDeleteMutation>
);
}}
</TypedProductListQuery>
)}
</AvailableInGridAttributesQuery>
); );
}; };
export default ProductList; export default ProductList;

View file

@ -11,6 +11,15 @@ import useNavigator from "@saleor/hooks/useNavigator";
import useNotifier from "@saleor/hooks/useNotifier"; import useNotifier from "@saleor/hooks/useNotifier";
import useShop from "@saleor/hooks/useShop"; import useShop from "@saleor/hooks/useShop";
import { commonMessages } from "@saleor/intl"; import { commonMessages } from "@saleor/intl";
import {
useProductDeleteMutation,
useProductImageCreateMutation,
useProductImageDeleteMutation,
useProductImagesReorder,
useProductUpdateMutation,
useProductVariantBulkDeleteMutation,
useSimpleProductUpdateMutation
} from "@saleor/products/mutations";
import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCategorySearch from "@saleor/searches/useCategorySearch";
import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
@ -20,14 +29,9 @@ import { FormattedMessage, useIntl } from "react-intl";
import { getMutationState, maybe } from "../../../misc"; import { getMutationState, maybe } from "../../../misc";
import ProductUpdatePage from "../../components/ProductUpdatePage"; import ProductUpdatePage from "../../components/ProductUpdatePage";
import ProductUpdateOperations from "../../containers/ProductUpdateOperations"; import { useProductDetails } from "../../queries";
import { TypedProductDetailsQuery } from "../../queries"; import { ProductImageCreateVariables } from "../../types/ProductImageCreate";
import {
ProductImageCreate,
ProductImageCreateVariables
} from "../../types/ProductImageCreate";
import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate"; import { ProductUpdate as ProductUpdateMutationResult } from "../../types/ProductUpdate";
import { ProductVariantBulkDelete } from "../../types/ProductVariantBulkDelete";
import { import {
productImageUrl, productImageUrl,
productListUrl, productListUrl,
@ -78,6 +82,86 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
}); });
const shop = useShop(); const shop = useShop();
const { data, loading, refetch } = useProductDetails({
displayLoader: true,
variables: {
id
}
});
const handleUpdate = (data: ProductUpdateMutationResult) => {
if (data.productUpdate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
}
};
const [updateProduct, updateProductOpts] = useProductUpdateMutation({
onCompleted: handleUpdate
});
const [
updateSimpleProduct,
updateSimpleProductOpts
] = useSimpleProductUpdateMutation({
onCompleted: handleUpdate
});
const [
reorderProductImages,
reorderProductImagesOpts
] = useProductImagesReorder({});
const [deleteProduct, deleteProductOpts] = useProductDeleteMutation({
onCompleted: () => {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Product removed"
})
});
navigate(productListUrl());
}
});
const [
createProductImage,
createProductImageOpts
] = useProductImageCreateMutation({
onCompleted: data => {
const imageError = data.productImageCreate.errors.find(
error => error.field === ("image" as keyof ProductImageCreateVariables)
);
if (imageError) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
}
});
const [deleteProductImage] = useProductImageDeleteMutation({
onCompleted: () =>
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
})
});
const [
bulkProductVariantDelete,
bulkProductVariantDeleteOpts
] = useProductVariantBulkDeleteMutation({
onCompleted: data => {
if (data.productVariantBulkDelete.errors.length === 0) {
closeModal();
reset();
refetch();
}
}
});
const [openModal, closeModal] = createDialogActionHandlers< const [openModal, closeModal] = createDialogActionHandlers<
ProductUrlDialog, ProductUrlDialog,
ProductUrlQueryParams ProductUrlQueryParams
@ -85,256 +169,169 @@ export const ProductUpdate: React.FC<ProductUpdateProps> = ({ id, params }) => {
const handleBack = () => navigate(productListUrl()); const handleBack = () => navigate(productListUrl());
const product = data?.product;
if (product === null) {
return <NotFoundPage onBack={handleBack} />;
}
const handleVariantAdd = () => navigate(productVariantAddUrl(id));
const handleImageDelete = (id: string) => () =>
deleteProductImage({ variables: { id } });
const handleImageEdit = (imageId: string) => () =>
navigate(productImageUrl(id, imageId));
const handleSubmit = createUpdateHandler(
product,
variables => updateProduct({ variables }),
variables => updateSimpleProduct({ variables })
);
const handleImageUpload = createImageUploadHandler(id, variables =>
createProductImage({ variables })
);
const handleImageReorder = createImageReorderHandler(product, variables =>
reorderProductImages({ variables })
);
const disableFormSave =
createProductImageOpts.loading ||
deleteProductOpts.loading ||
reorderProductImagesOpts.loading ||
updateProductOpts.loading ||
loading;
const formTransitionState = getMutationState(
updateProductOpts.called || updateSimpleProductOpts.called,
updateProductOpts.loading || updateSimpleProductOpts.loading,
maybe(() => updateProductOpts.data.productUpdate.errors),
maybe(() => updateSimpleProductOpts.data.productUpdate.errors),
maybe(() => updateSimpleProductOpts.data.productVariantUpdate.errors)
);
const categories = maybe(
() => searchCategoriesOpts.data.search.edges,
[]
).map(edge => edge.node);
const collections = maybe(
() => searchCollectionsOpts.data.search.edges,
[]
).map(edge => edge.node);
const errors = [
...maybe(() => updateProductOpts.data.productUpdate.errors, []),
...maybe(() => updateSimpleProductOpts.data.productUpdate.errors, [])
];
return ( return (
<TypedProductDetailsQuery displayLoader variables={{ id }}> <>
{({ data, loading, refetch }) => { <WindowTitle title={maybe(() => data.product.name)} />
const product = data?.product; <ProductUpdatePage
categories={categories}
if (product === null) { collections={collections}
return <NotFoundPage onBack={handleBack} />; defaultWeightUnit={shop?.defaultWeightUnit}
disabled={disableFormSave}
errors={errors}
fetchCategories={searchCategories}
fetchCollections={searchCollections}
saveButtonBarState={formTransitionState}
images={maybe(() => data.product.images)}
header={maybe(() => product.name)}
placeholderImage={placeholderImg}
product={product}
warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
} }
variants={maybe(() => product.variants)}
const handleDelete = () => { onBack={handleBack}
notify({ onDelete={() => openModal("remove")}
status: "success", onImageReorder={handleImageReorder}
text: intl.formatMessage({ onSubmit={handleSubmit}
defaultMessage: "Product removed" onVariantAdd={handleVariantAdd}
}) onVariantsAdd={() => navigate(productVariantCreatorUrl(id))}
}); onVariantShow={variantId => () =>
navigate(productListUrl()); navigate(productVariantEditUrl(product.id, variantId))}
}; onImageUpload={handleImageUpload}
const handleUpdate = (data: ProductUpdateMutationResult) => { onImageEdit={handleImageEdit}
if (data.productUpdate.errors.length === 0) { onImageDelete={handleImageDelete}
notify({ toolbar={
status: "success", <IconButton
text: intl.formatMessage(commonMessages.savedChanges) color="primary"
}); onClick={() =>
} openModal("remove-variants", {
}; ids: listElements
})
const handleImageCreate = (data: ProductImageCreate) => { }
const imageError = data.productImageCreate.errors.find(
error =>
error.field === ("image" as keyof ProductImageCreateVariables)
);
if (imageError) {
notify({
status: "error",
text: intl.formatMessage(commonMessages.somethingWentWrong)
});
}
};
const handleImageDeleteSuccess = () =>
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
const handleVariantAdd = () => navigate(productVariantAddUrl(id));
const handleBulkProductVariantDelete = (
data: ProductVariantBulkDelete
) => {
if (data.productVariantBulkDelete.errors.length === 0) {
closeModal();
reset();
refetch();
}
};
return (
<ProductUpdateOperations
product={product}
onBulkProductVariantDelete={handleBulkProductVariantDelete}
onDelete={handleDelete}
onImageCreate={handleImageCreate}
onImageDelete={handleImageDeleteSuccess}
onUpdate={handleUpdate}
> >
{({ <DeleteIcon />
bulkProductVariantDelete, </IconButton>
createProductImage, }
deleteProduct, isChecked={isSelected}
deleteProductImage, selected={listElements.length}
reorderProductImages, toggle={toggle}
updateProduct, toggleAll={toggleAll}
updateSimpleProduct fetchMoreCategories={{
}) => { hasMore: maybe(
const handleImageDelete = (id: string) => () => () => searchCategoriesOpts.data.search.pageInfo.hasNextPage
deleteProductImage.mutate({ id }); ),
const handleImageEdit = (imageId: string) => () => loading: searchCategoriesOpts.loading,
navigate(productImageUrl(id, imageId)); onFetchMore: loadMoreCategories
const handleSubmit = createUpdateHandler( }}
product, fetchMoreCollections={{
updateProduct.mutate, hasMore: maybe(
updateSimpleProduct.mutate () => searchCollectionsOpts.data.search.pageInfo.hasNextPage
); ),
const handleImageUpload = createImageUploadHandler( loading: searchCollectionsOpts.loading,
id, onFetchMore: loadMoreCollections
createProductImage.mutate }}
); />
const handleImageReorder = createImageReorderHandler( <ActionDialog
product, open={params.action === "remove"}
reorderProductImages.mutate onClose={closeModal}
); confirmButtonState={deleteProductOpts.status}
onConfirm={() => deleteProduct({ variables: { id } })}
const disableFormSave = variant="delete"
createProductImage.opts.loading || title={intl.formatMessage({
deleteProduct.opts.loading || defaultMessage: "Delete Product",
reorderProductImages.opts.loading || description: "dialog header"
updateProduct.opts.loading || })}
loading; >
const formTransitionState = getMutationState( <DialogContentText>
updateProduct.opts.called || updateSimpleProduct.opts.called, <FormattedMessage
updateProduct.opts.loading || updateSimpleProduct.opts.loading, defaultMessage="Are you sure you want to delete {name}?"
maybe(() => updateProduct.opts.data.productUpdate.errors), description="delete product"
maybe(() => updateSimpleProduct.opts.data.productUpdate.errors), values={{
maybe( name: product ? product.name : undefined
() =>
updateSimpleProduct.opts.data.productVariantUpdate.errors
)
);
const categories = maybe(
() => searchCategoriesOpts.data.search.edges,
[]
).map(edge => edge.node);
const collections = maybe(
() => searchCollectionsOpts.data.search.edges,
[]
).map(edge => edge.node);
const errors = [
...maybe(
() => updateProduct.opts.data.productUpdate.errors,
[]
),
...maybe(
() => updateSimpleProduct.opts.data.productUpdate.errors,
[]
)
];
return (
<>
<WindowTitle title={maybe(() => data.product.name)} />
<ProductUpdatePage
categories={categories}
collections={collections}
defaultWeightUnit={shop?.defaultWeightUnit}
disabled={disableFormSave}
errors={errors}
fetchCategories={searchCategories}
fetchCollections={searchCollections}
saveButtonBarState={formTransitionState}
images={maybe(() => data.product.images)}
header={maybe(() => product.name)}
placeholderImage={placeholderImg}
product={product}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
variants={maybe(() => product.variants)}
onBack={handleBack}
onDelete={() => openModal("remove")}
onImageReorder={handleImageReorder}
onSubmit={handleSubmit}
onVariantAdd={handleVariantAdd}
onVariantsAdd={() => navigate(productVariantCreatorUrl(id))}
onVariantShow={variantId => () =>
navigate(productVariantEditUrl(product.id, variantId))}
onImageUpload={handleImageUpload}
onImageEdit={handleImageEdit}
onImageDelete={handleImageDelete}
toolbar={
<IconButton
color="primary"
onClick={() =>
openModal("remove-variants", {
ids: listElements
})
}
>
<DeleteIcon />
</IconButton>
}
isChecked={isSelected}
selected={listElements.length}
toggle={toggle}
toggleAll={toggleAll}
fetchMoreCategories={{
hasMore: maybe(
() =>
searchCategoriesOpts.data.search.pageInfo.hasNextPage
),
loading: searchCategoriesOpts.loading,
onFetchMore: loadMoreCategories
}}
fetchMoreCollections={{
hasMore: maybe(
() =>
searchCollectionsOpts.data.search.pageInfo.hasNextPage
),
loading: searchCollectionsOpts.loading,
onFetchMore: loadMoreCollections
}}
/>
<ActionDialog
open={params.action === "remove"}
onClose={closeModal}
confirmButtonState={deleteProduct.opts.status}
onConfirm={() => deleteProduct.mutate({ id })}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="Are you sure you want to delete {name}?"
description="delete product"
values={{
name: product ? product.name : undefined
}}
/>
</DialogContentText>
</ActionDialog>
<ActionDialog
open={params.action === "remove-variants"}
onClose={closeModal}
confirmButtonState={bulkProductVariantDelete.opts.status}
onConfirm={() =>
bulkProductVariantDelete.mutate({
ids: params.ids
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: (
<strong>{maybe(() => params.ids.length)}</strong>
)
}}
/>
</DialogContentText>
</ActionDialog>
</>
);
}} }}
</ProductUpdateOperations> />
); </DialogContentText>
}} </ActionDialog>
</TypedProductDetailsQuery> <ActionDialog
open={params.action === "remove-variants"}
onClose={closeModal}
confirmButtonState={bulkProductVariantDeleteOpts.status}
onConfirm={() =>
bulkProductVariantDelete({
variables: {
ids: params.ids
}
})
}
variant="delete"
title={intl.formatMessage({
defaultMessage: "Delete Product Variants",
description: "dialog header"
})}
>
<DialogContentText>
<FormattedMessage
defaultMessage="{counter,plural,one{Are you sure you want to delete this variant?} other{Are you sure you want to delete {displayQuantity} variants?}}"
description="dialog content"
values={{
counter: maybe(() => params.ids.length),
displayQuantity: <strong>{maybe(() => params.ids.length)}</strong>
}}
/>
</DialogContentText>
</ActionDialog>
</>
); );
}; };
export default ProductUpdate; export default ProductUpdate;

View file

@ -15,12 +15,14 @@ import ProductVariantDeleteDialog from "../components/ProductVariantDeleteDialog
import ProductVariantPage, { import ProductVariantPage, {
ProductVariantPageSubmitData ProductVariantPageSubmitData
} from "../components/ProductVariantPage"; } from "../components/ProductVariantPage";
import ProductVariantOperations from "../containers/ProductVariantOperations";
import { TypedProductVariantQuery } from "../queries";
import { import {
VariantUpdate, useVariantDeleteMutation,
VariantUpdate_productVariantUpdate_errors useVariantImageAssignMutation,
} from "../types/VariantUpdate"; useVariantImageUnassignMutation,
useVariantUpdateMutation
} from "../mutations";
import { useProductVariantQuery } from "../queries";
import { VariantUpdate_productVariantUpdate_errors } from "../types/VariantUpdate";
import { import {
productUrl, productUrl,
productVariantAddUrl, productVariantAddUrl,
@ -59,6 +61,13 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
} }
}); });
const { data, loading } = useProductVariantQuery({
displayLoader: true,
variables: {
id: variantId
}
});
const [openModal] = createDialogActionHandlers< const [openModal] = createDialogActionHandlers<
ProductVariantEditUrlDialog, ProductVariantEditUrlDialog,
ProductVariantEditUrlQueryParams ProductVariantEditUrlQueryParams
@ -70,132 +79,122 @@ export const ProductVariant: React.FC<ProductUpdateProps> = ({
const handleBack = () => navigate(productUrl(productId)); const handleBack = () => navigate(productUrl(productId));
return ( const [assignImage, assignImageOpts] = useVariantImageAssignMutation({});
<TypedProductVariantQuery displayLoader variables={{ id: variantId }}> const [unassignImage, unassignImageOpts] = useVariantImageUnassignMutation(
{({ data, loading }) => { {}
const variant = data?.productVariant; );
const [deleteVariant, deleteVariantOpts] = useVariantDeleteMutation({
onCompleted: () => {
notify({
status: "success",
text: intl.formatMessage({
defaultMessage: "Variant removed"
})
});
navigate(productUrl(productId));
}
});
const [updateVariant, updateVariantOpts] = useVariantUpdateMutation({
onCompleted: data => {
if (data.productVariantUpdate.errors.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges)
});
} else {
setErrors(data.productVariantUpdate.errors);
}
}
});
if (variant === null) { const variant = data?.productVariant;
return <NotFoundPage onBack={handleBack} />;
}
const handleDelete = () => { if (variant === null) {
notify({ return <NotFoundPage onBack={handleBack} />;
status: "success", }
text: intl.formatMessage({
defaultMessage: "Variant removed" const disableFormSave =
}) loading ||
}); deleteVariantOpts.loading ||
navigate(productUrl(productId)); updateVariantOpts.loading ||
}; assignImageOpts.loading ||
const handleUpdate = (data: VariantUpdate) => { unassignImageOpts.loading;
if (data.productVariantUpdate.errors.length === 0) {
notify({ const handleImageSelect = (id: string) => () => {
status: "success", if (variant) {
text: intl.formatMessage(commonMessages.savedChanges) if (variant?.images?.map(image => image.id).indexOf(id) !== -1) {
}); unassignImage({
} else { variables: {
setErrors(data.productVariantUpdate.errors); imageId: id,
variantId: variant.id
} }
}; });
} else {
assignImage({
variables: {
imageId: id,
variantId: variant.id
}
});
}
}
};
return ( return (
<ProductVariantOperations <>
onDelete={handleDelete} <WindowTitle title={data?.productVariant?.name} />
onUpdate={handleUpdate} <ProductVariantPage
> defaultWeightUnit={shop?.defaultWeightUnit}
{({ assignImage, deleteVariant, updateVariant, unassignImage }) => { errors={errors}
const disableFormSave = saveButtonBarState={updateVariantOpts.status}
loading || loading={disableFormSave}
deleteVariant.opts.loading || placeholderImage={placeholderImg}
updateVariant.opts.loading || variant={variant}
assignImage.opts.loading || header={variant?.name || variant?.sku}
unassignImage.opts.loading; warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
const handleImageSelect = (id: string) => () => { }
if (variant) { onAdd={() => navigate(productVariantAddUrl(productId))}
if ( onBack={handleBack}
variant.images && onDelete={() => openModal("remove")}
variant.images.map(image => image.id).indexOf(id) !== -1 onImageSelect={handleImageSelect}
) { onSubmit={(data: ProductVariantPageSubmitData) =>
unassignImage.mutate({ updateVariant({
imageId: id, variables: {
variantId: variant.id addStocks: data.addStocks.map(mapFormsetStockToStockInput),
}); attributes: data.attributes.map(attribute => ({
} else { id: attribute.id,
assignImage.mutate({ values: [attribute.value]
imageId: id, })),
variantId: variant.id costPrice: decimal(data.costPrice),
}); id: variantId,
} price: decimal(data.price),
} removeStocks: data.removeStocks,
}; sku: data.sku,
stocks: data.updateStocks.map(mapFormsetStockToStockInput),
return ( trackInventory: data.trackInventory,
<> weight: weight(data.weight)
<WindowTitle title={data?.productVariant?.name} /> }
<ProductVariantPage })
defaultWeightUnit={shop?.defaultWeightUnit} }
errors={errors} onVariantClick={variantId => {
saveButtonBarState={updateVariant.opts.status} navigate(productVariantEditUrl(productId, variantId));
loading={disableFormSave} }}
placeholderImage={placeholderImg} />
variant={variant} <ProductVariantDeleteDialog
header={variant?.name || variant?.sku} confirmButtonState={deleteVariantOpts.status}
warehouses={ onClose={() => navigate(productVariantEditUrl(productId, variantId))}
warehouses.data?.warehouses.edges.map( onConfirm={() =>
edge => edge.node deleteVariant({
) || [] variables: {
} id: variantId
onAdd={() => navigate(productVariantAddUrl(productId))} }
onBack={handleBack} })
onDelete={() => openModal("remove")} }
onImageSelect={handleImageSelect} open={params.action === "remove"}
onSubmit={(data: ProductVariantPageSubmitData) => name={data?.productVariant?.name}
updateVariant.mutate({ />
addStocks: data.addStocks.map( </>
mapFormsetStockToStockInput
),
attributes: data.attributes.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(data.costPrice),
id: variantId,
price: decimal(data.price),
removeStocks: data.removeStocks,
sku: data.sku,
stocks: data.updateStocks.map(
mapFormsetStockToStockInput
),
trackInventory: data.trackInventory,
weight: weight(data.weight)
})
}
onVariantClick={variantId => {
navigate(productVariantEditUrl(productId, variantId));
}}
/>
<ProductVariantDeleteDialog
confirmButtonState={deleteVariant.opts.status}
onClose={() =>
navigate(productVariantEditUrl(productId, variantId))
}
onConfirm={() =>
deleteVariant.mutate({
id: variantId
})
}
open={params.action === "remove"}
name={data?.productVariant?.name}
/>
</>
);
}}
</ProductVariantOperations>
);
}}
</TypedProductVariantQuery>
); );
}; };
export default ProductVariant; export default ProductVariant;

View file

@ -12,9 +12,8 @@ import { decimal, weight } from "../../misc";
import ProductVariantCreatePage, { import ProductVariantCreatePage, {
ProductVariantCreatePageSubmitData ProductVariantCreatePageSubmitData
} from "../components/ProductVariantCreatePage"; } from "../components/ProductVariantCreatePage";
import { TypedVariantCreateMutation } from "../mutations"; import { useVariantCreateMutation } from "../mutations";
import { TypedProductVariantCreateQuery } from "../queries"; import { useProductVariantCreateQuery } from "../queries";
import { VariantCreate } from "../types/VariantCreate";
import { productListUrl, productUrl, productVariantEditUrl } from "../urls"; import { productListUrl, productUrl, productVariantEditUrl } from "../urls";
interface ProductVariantCreateProps { interface ProductVariantCreateProps {
@ -35,102 +34,90 @@ export const ProductVariant: React.FC<ProductVariantCreateProps> = ({
} }
}); });
return ( const { data, loading: productLoading } = useProductVariantCreateQuery({
<TypedProductVariantCreateQuery displayLoader variables={{ id: productId }}> displayLoader: true,
{({ data, loading: productLoading }) => { variables: { id: productId }
const product = data?.product; });
if (product === null) { const [variantCreate, variantCreateResult] = useVariantCreateMutation({
return <NotFoundPage onBack={() => navigate(productListUrl())} />; onCompleted: data => {
} if (data.productVariantCreate.errors.length === 0) {
notify({
const handleCreateSuccess = (data: VariantCreate) => { status: "success",
if (data.productVariantCreate.errors.length === 0) { text: intl.formatMessage(commonMessages.savedChanges)
notify({ });
status: "success", navigate(
text: intl.formatMessage(commonMessages.savedChanges) productVariantEditUrl(
}); productId,
navigate( data.productVariantCreate.productVariant.id
productVariantEditUrl( )
productId,
data.productVariantCreate.productVariant.id
)
);
}
};
return (
<TypedVariantCreateMutation onCompleted={handleCreateSuccess}>
{(variantCreate, variantCreateResult) => {
const handleBack = () => navigate(productUrl(productId));
const handleSubmit = (
formData: ProductVariantCreatePageSubmitData
) =>
variantCreate({
variables: {
input: {
attributes: formData.attributes
.filter(attribute => attribute.value !== "")
.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(formData.costPrice),
price: decimal(formData.price),
product: productId,
sku: formData.sku,
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
})),
trackInventory: true,
weight: weight(formData.weight)
}
}
});
const handleVariantClick = (id: string) =>
navigate(productVariantEditUrl(productId, id));
const disableForm = productLoading || variantCreateResult.loading;
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create variant",
description: "window title"
})}
/>
<ProductVariantCreatePage
currencySymbol={shop?.defaultCurrency}
disabled={disableForm}
errors={
variantCreateResult.data?.productVariantCreate.errors ||
[]
}
header={intl.formatMessage({
defaultMessage: "Create Variant",
description: "header"
})}
product={data?.product}
onBack={handleBack}
onSubmit={handleSubmit}
onVariantClick={handleVariantClick}
saveButtonBarState={variantCreateResult.status}
warehouses={
warehouses.data?.warehouses.edges.map(
edge => edge.node
) || []
}
weightUnit={shop?.defaultWeightUnit}
/>
</>
);
}}
</TypedVariantCreateMutation>
); );
}} }
</TypedProductVariantCreateQuery> }
});
const product = data?.product;
if (product === null) {
return <NotFoundPage onBack={() => navigate(productListUrl())} />;
}
const handleBack = () => navigate(productUrl(productId));
const handleSubmit = (formData: ProductVariantCreatePageSubmitData) =>
variantCreate({
variables: {
input: {
attributes: formData.attributes
.filter(attribute => attribute.value !== "")
.map(attribute => ({
id: attribute.id,
values: [attribute.value]
})),
costPrice: decimal(formData.costPrice),
price: decimal(formData.price),
product: productId,
sku: formData.sku,
stocks: formData.stocks.map(stock => ({
quantity: parseInt(stock.value, 0),
warehouse: stock.id
})),
trackInventory: true,
weight: weight(formData.weight)
}
}
});
const handleVariantClick = (id: string) =>
navigate(productVariantEditUrl(productId, id));
const disableForm = productLoading || variantCreateResult.loading;
return (
<>
<WindowTitle
title={intl.formatMessage({
defaultMessage: "Create variant",
description: "window title"
})}
/>
<ProductVariantCreatePage
currencySymbol={shop?.defaultCurrency}
disabled={disableForm}
errors={variantCreateResult.data?.productVariantCreate.errors || []}
header={intl.formatMessage({
defaultMessage: "Create Variant",
description: "header"
})}
product={data?.product}
onBack={handleBack}
onSubmit={handleSubmit}
onVariantClick={handleVariantClick}
saveButtonBarState={variantCreateResult.status}
warehouses={
warehouses.data?.warehouses.edges.map(edge => edge.node) || []
}
weightUnit={shop?.defaultWeightUnit}
/>
</>
); );
}; };
export default ProductVariant; export default ProductVariant;