From 4b661c9bffa59d5a782f485650d7ef7940dccaeb Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Wed, 25 Mar 2020 14:06:14 +0100 Subject: [PATCH] Add simple product stock management --- src/categories/views/CategoryDetails.tsx | 2 +- .../Navigator/modes/commands/actions.ts | 2 +- .../ProductCreatePage/ProductCreatePage.tsx | 18 ++++- .../ProductStocks/ProductStocks.tsx | 56 +++++++++----- .../ProductWarehousesDialog.tsx | 11 ++- src/products/index.tsx | 12 ++- src/products/mutations.ts | 2 + src/products/types/ProductCreate.ts | 3 +- src/products/urls.ts | 5 +- src/products/views/ProductCreate.tsx | 73 +++++++++++++++++-- .../views/ProductList/ProductList.tsx | 2 +- .../views/ProductUpdate/ProductUpdate.tsx | 6 +- 12 files changed, 150 insertions(+), 42 deletions(-) diff --git a/src/categories/views/CategoryDetails.tsx b/src/categories/views/CategoryDetails.tsx index e33d91b96..5e8f5ab12 100644 --- a/src/categories/views/CategoryDetails.tsx +++ b/src/categories/views/CategoryDetails.tsx @@ -172,7 +172,7 @@ export const CategoryDetails: React.FC = ({ disabled={loading} errors={updateResult.data?.categoryUpdate.errors || []} onAddCategory={() => navigate(categoryAddUrl(id))} - onAddProduct={() => navigate(productAddUrl)} + onAddProduct={() => navigate(productAddUrl())} onBack={() => navigate( maybe( diff --git a/src/components/Navigator/modes/commands/actions.ts b/src/components/Navigator/modes/commands/actions.ts index 6a85d568a..9956ba8b1 100644 --- a/src/components/Navigator/modes/commands/actions.ts +++ b/src/components/Navigator/modes/commands/actions.ts @@ -46,7 +46,7 @@ export function searchInCommands( { label: intl.formatMessage(messages.createProduct), onClick: () => { - navigate(productAddUrl); + navigate(productAddUrl()); return false; } }, diff --git a/src/products/components/ProductCreatePage/ProductCreatePage.tsx b/src/products/components/ProductCreatePage/ProductCreatePage.tsx index e3f6d61a5..b7538c25d 100644 --- a/src/products/components/ProductCreatePage/ProductCreatePage.tsx +++ b/src/products/components/ProductCreatePage/ProductCreatePage.tsx @@ -28,6 +28,7 @@ import { SearchProductTypes_search_edges_node_productAttributes } from "@saleor/ import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import { ProductErrorFragment } from "@saleor/attributes/types/ProductErrorFragment"; +import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { FetchMoreProps } from "../../../types"; import { createAttributeChangeHandler, @@ -81,6 +82,7 @@ interface ProductCreatePageProps { }>; header: string; saveButtonBarState: ConfirmButtonTransitionState; + warehouses: SearchWarehouses_search_edges_node[]; fetchCategories: (data: string) => void; fetchCollections: (data: string) => void; fetchProductTypes: (data: string) => void; @@ -103,6 +105,7 @@ export const ProductCreatePage: React.FC = ({ header, productTypes: productTypeChoiceList, saveButtonBarState, + warehouses, onBack, fetchProductTypes, onSubmit, @@ -116,7 +119,18 @@ export const ProductCreatePage: React.FC = ({ data: attributes, set: setAttributeData } = useFormset([]); - const { change: changeStockData, data: stocks } = useFormset([]); + const { change: changeStockData, data: stocks, set: setStocks } = useFormset< + null + >([]); + React.useEffect(() => { + const newStocks = warehouses.map(warehouse => ({ + data: null, + id: warehouse.id, + label: warehouse.name, + value: stocks.find(stock => stock.id === warehouse.id)?.value || 0 + })); + setStocks(newStocks); + }, [JSON.stringify(warehouses)]); // Ensures that it will not change after component rerenders, because it // generates different block keys and it causes editor to lose its content. @@ -248,7 +262,7 @@ export const ProductCreatePage: React.FC = ({ onFormDataChange={change} errors={errors} stocks={stocks} - onWarehouseEdit={onWarehouseEdit} + onWarehousesEdit={onWarehouseEdit} /> diff --git a/src/products/components/ProductStocks/ProductStocks.tsx b/src/products/components/ProductStocks/ProductStocks.tsx index 02f14b64a..54c770223 100644 --- a/src/products/components/ProductStocks/ProductStocks.tsx +++ b/src/products/components/ProductStocks/ProductStocks.tsx @@ -21,6 +21,7 @@ import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import FormSpacer from "@saleor/components/FormSpacer"; import Hr from "@saleor/components/Hr"; import { renderCollection } from "@saleor/misc"; +import Link from "@saleor/components/Link"; export type ProductStockInput = FormsetAtomicData; export interface ProductStockFormData { @@ -35,7 +36,7 @@ export interface ProductStocksProps { stocks: ProductStockInput[]; onChange: FormsetChange; onFormDataChange: FormChange; - onWarehousesEdit: () => undefined; + onWarehousesEdit: () => void; } const useStyles = makeStyles( @@ -171,24 +172,41 @@ const ProductStocks: React.FC = ({ - {renderCollection(stocks, stock => ( - - {stock.label} - - onChange(stock.id, event.target.value)} - value={stock.value} - /> - - - ))} + {renderCollection( + stocks, + stock => ( + + {stock.label} + + onChange(stock.id, event.target.value)} + value={stock.value} + /> + + + ), + () => ( + + + here." + } + values={{ + l: str => {str} + }} + /> + + + ) + )} diff --git a/src/products/components/ProductWarehousesDialog/ProductWarehousesDialog.tsx b/src/products/components/ProductWarehousesDialog/ProductWarehousesDialog.tsx index 9faaa1be9..a975f31bb 100644 --- a/src/products/components/ProductWarehousesDialog/ProductWarehousesDialog.tsx +++ b/src/products/components/ProductWarehousesDialog/ProductWarehousesDialog.tsx @@ -15,7 +15,6 @@ import ConfirmButton, { import { buttonMessages } from "@saleor/intl"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import Skeleton from "@saleor/components/Skeleton"; -import { Product_variants_stocks } from "@saleor/products/types/Product"; import ControlledCheckbox from "@saleor/components/ControlledCheckbox"; import { isSelected, toggle } from "@saleor/utils/lists"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; @@ -44,8 +43,8 @@ export interface ProductWarehousesDialogProps { disabled: boolean; errors: Array; open: boolean; - stocks: Product_variants_stocks[]; warehouses: SearchWarehouses_search_edges_node[]; + warehousesWithStocks: string[]; onClose: () => void; onConfirm: (data: DiffData) => void; } @@ -57,18 +56,18 @@ const ProductWarehousesDialog: React.FC = ({ onClose, onConfirm, open, - stocks, + warehousesWithStocks, warehouses }) => { const classes = useStyles({}); const intl = useIntl(); - const initial = stocks?.map(stock => stock.warehouse.id) || []; const [selectedWarehouses, setSelectedWarehouses] = useStateFromProps( - initial + warehousesWithStocks || [] ); - const handleConfirm = () => onConfirm(diff(initial, selectedWarehouses)); + const handleConfirm = () => + onConfirm(diff(warehousesWithStocks, selectedWarehouses)); return ( diff --git a/src/products/index.tsx b/src/products/index.tsx index 9db230308..c17a75799 100644 --- a/src/products/index.tsx +++ b/src/products/index.tsx @@ -18,9 +18,10 @@ import { ProductUrlQueryParams, productVariantAddPath, productVariantEditPath, - ProductVariantEditUrlQueryParams + ProductVariantEditUrlQueryParams, + ProductAddUrlQueryParams } from "./urls"; -import ProductCreate from "./views/ProductCreate"; +import ProductCreateComponent from "./views/ProductCreate"; import ProductImageComponent from "./views/ProductImage"; import ProductListComponent from "./views/ProductList"; import ProductUpdateComponent from "./views/ProductUpdate"; @@ -92,6 +93,13 @@ const ProductVariantCreate: React.FC> = ({ /> ); +const ProductCreate: React.FC = ({ location }) => { + const qs = parseQs(location.search.substr(1)); + const params: ProductAddUrlQueryParams = qs; + + return ; +}; + const Component = () => { const intl = useIntl(); diff --git a/src/products/mutations.ts b/src/products/mutations.ts index 98247c432..48c625ec2 100644 --- a/src/products/mutations.ts +++ b/src/products/mutations.ts @@ -302,6 +302,7 @@ export const productCreateMutation = gql` $sku: String $stockQuantity: Int $seo: SeoInput + $stocks: [StockInput!]! ) { productCreate( input: { @@ -318,6 +319,7 @@ export const productCreateMutation = gql` sku: $sku quantity: $stockQuantity seo: $seo + stocks: $stocks } ) { errors: productErrors { diff --git a/src/products/types/ProductCreate.ts b/src/products/types/ProductCreate.ts index 32cdd2304..a38b7cf40 100644 --- a/src/products/types/ProductCreate.ts +++ b/src/products/types/ProductCreate.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // This file was automatically generated and should not be edited. -import { AttributeValueInput, SeoInput, ProductErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes"; +import { AttributeValueInput, SeoInput, StockInput, ProductErrorCode, AttributeInputTypeEnum } from "./../../types/globalTypes"; // ==================================================== // GraphQL mutation operation: ProductCreate @@ -211,4 +211,5 @@ export interface ProductCreateVariables { sku?: string | null; stockQuantity?: number | null; seo?: SeoInput | null; + stocks: StockInput[]; } diff --git a/src/products/urls.ts b/src/products/urls.ts index 5cf5e5d95..17ecded15 100644 --- a/src/products/urls.ts +++ b/src/products/urls.ts @@ -17,7 +17,10 @@ import { const productSection = "/products/"; export const productAddPath = urlJoin(productSection, "add"); -export const productAddUrl = productAddPath; +export type ProductAddUrlDialog = "edit-stocks"; +export type ProductAddUrlQueryParams = Dialog; +export const productAddUrl = (params?: ProductAddUrlQueryParams): string => + productAddPath + "?" + stringifyQs(params); export const productListPath = productSection; export type ProductListUrlDialog = diff --git a/src/products/views/ProductCreate.tsx b/src/products/views/ProductCreate.tsx index ae7b565e6..94d77c116 100644 --- a/src/products/views/ProductCreate.tsx +++ b/src/products/views/ProductCreate.tsx @@ -9,19 +9,31 @@ import useShop from "@saleor/hooks/useShop"; import useCategorySearch from "@saleor/searches/useCategorySearch"; import useCollectionSearch from "@saleor/searches/useCollectionSearch"; import useProductTypeSearch from "@saleor/searches/useProductTypeSearch"; +import useWarehouseSearch from "@saleor/searches/useWarehouseSearch"; +import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers"; +import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; import { decimal, maybe } from "../../misc"; import ProductCreatePage, { ProductCreatePageSubmitData } from "../components/ProductCreatePage"; import { TypedProductCreateMutation } from "../mutations"; import { ProductCreate } from "../types/ProductCreate"; -import { productListUrl, productUrl } from "../urls"; +import { + productListUrl, + productUrl, + ProductAddUrlDialog, + ProductAddUrlQueryParams, + productAddUrl +} from "../urls"; +import ProductWarehousesDialog from "../components/ProductWarehousesDialog"; -interface ProductUpdateProps { - id: string; +interface ProductCreateViewProps { + params: ProductAddUrlQueryParams; } -export const ProductUpdate: React.FC = () => { +export const ProductCreateView: React.FC = ({ + params +}) => { const navigate = useNavigator(); const notify = useNotifier(); const shop = useShop(); @@ -47,6 +59,24 @@ export const ProductUpdate: React.FC = () => { } = useProductTypeSearch({ variables: DEFAULT_INITIAL_SEARCH_DATA }); + const { + loadMore: loadMoreWarehouses, + search: searchWarehouses, + result: searchWarehousesOpts + } = useWarehouseSearch({ + variables: { + ...DEFAULT_INITIAL_SEARCH_DATA, + first: 20 + } + }); + const [warehouses, setWarehouses] = React.useState< + SearchWarehouses_search_edges_node[] + >([]); + + const [openModal, closeModal] = createDialogActionHandlers< + ProductAddUrlDialog, + ProductAddUrlQueryParams + >(navigate, productAddUrl, params); const handleBack = () => navigate(productListUrl()); @@ -88,8 +118,10 @@ export const ProductUpdate: React.FC = () => { title: formData.seoTitle }, sku: formData.sku, - stockQuantity: - formData.stockQuantity !== null ? formData.stockQuantity : 0 + stocks: formData.stocks.map(stock => ({ + quantity: parseInt(stock.value, 0), + warehouse: stock.id + })) } }); }; @@ -124,6 +156,7 @@ export const ProductUpdate: React.FC = () => { productTypes={maybe(() => searchProductTypesOpts.data.search.edges.map(edge => edge.node) )} + warehouses={warehouses} onBack={handleBack} onSubmit={handleSubmit} saveButtonBarState={productCreateOpts.status} @@ -148,6 +181,32 @@ export const ProductUpdate: React.FC = () => { loading: searchProductTypesOpts.loading, onFetchMore: loadMoreProductTypes }} + onWarehouseEdit={() => openModal("edit-stocks")} + /> + edge.node + )} + warehousesWithStocks={warehouses.map(warehouse => warehouse.id)} + onConfirm={data => { + setWarehouses( + [ + ...warehouses, + ...data.added.map( + addedId => + searchWarehousesOpts.data.search.edges.find( + edge => edge.node.id === addedId + ).node + ) + ].filter(warehouse => !data.removed.includes(warehouse.id)) + ); + closeModal(); + }} /> ); @@ -155,4 +214,4 @@ export const ProductUpdate: React.FC = () => { ); }; -export default ProductUpdate; +export default ProductCreateView; diff --git a/src/products/views/ProductList/ProductList.tsx b/src/products/views/ProductList/ProductList.tsx index de9ebe432..302006ca0 100644 --- a/src/products/views/ProductList/ProductList.tsx +++ b/src/products/views/ProductList/ProductList.tsx @@ -306,7 +306,7 @@ export const ProductList: React.FC = ({ params }) => { .hasNextPage, false )} - onAdd={() => navigate(productAddUrl)} + onAdd={() => navigate(productAddUrl())} disabled={loading} products={maybe(() => data.products.edges.map(edge => edge.node) diff --git a/src/products/views/ProductUpdate/ProductUpdate.tsx b/src/products/views/ProductUpdate/ProductUpdate.tsx index 9c0433895..477e41c69 100644 --- a/src/products/views/ProductUpdate/ProductUpdate.tsx +++ b/src/products/views/ProductUpdate/ProductUpdate.tsx @@ -384,11 +384,15 @@ export const ProductUpdate: React.FC = ({ id, params }) => { ?.productVariantStocksDelete.errors || []) ]} onClose={closeModal} - stocks={product?.variants[0].stocks || []} open={params.action === "edit-stocks"} warehouses={searchWarehousesOpts.data?.search.edges.map( edge => edge.node )} + warehousesWithStocks={ + product?.variants[0].stocks.map( + stock => stock.warehouse.id + ) || [] + } onConfirm={data => addOrRemoveStocks({ variables: {