diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx index 6975e4615..ca8d90beb 100644 --- a/src/products/components/ProductUpdatePage/form.tsx +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -17,6 +17,7 @@ import { createAttributeMultiChangeHandler } from "@saleor/products/utils/handlers"; import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; +import handleFormSubmit from "@saleor/utils/handlers/handleFormSubmit"; import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; diff --git a/src/products/components/ProductVariantCreatePage/form.tsx b/src/products/components/ProductVariantCreatePage/form.tsx index 5e7e443e8..668bcbdf4 100644 --- a/src/products/components/ProductVariantCreatePage/form.tsx +++ b/src/products/components/ProductVariantCreatePage/form.tsx @@ -16,13 +16,12 @@ import { VariantAttributeInputData } from "../ProductVariantAttributes"; export interface ProductVariantCreateFormData extends MetadataFormData { costPrice: string; price: string; - quantity: string; sku: string; trackInventory: boolean; weight: string; } export interface ProductVariantCreateData extends ProductVariantCreateFormData { - attributes: FormsetData; + attributes: FormsetData; stocks: ProductStockInput[]; } @@ -33,7 +32,7 @@ export interface UseProductVariantCreateFormOpts { export interface UseProductVariantCreateFormResult { change: FormChange; data: ProductVariantCreateData; - handlers: Record<"changeStock" | "selectAttribute", FormsetChange> & + handlers: Record<"changeStock" | "selectAttribute", FormsetChange> & Record<"addStock" | "deleteStock", (id: string) => void> & { changeMetadata: FormChange; }; @@ -53,7 +52,6 @@ const initial: ProductVariantCreateFormData = { metadata: [], price: "", privateMetadata: [], - quantity: "0", sku: "", trackInventory: true, weight: "" diff --git a/src/products/components/ProductVariantPage/ProductVariantPage.tsx b/src/products/components/ProductVariantPage/ProductVariantPage.tsx index f70fdca38..e2fa15970 100644 --- a/src/products/components/ProductVariantPage/ProductVariantPage.tsx +++ b/src/products/components/ProductVariantPage/ProductVariantPage.tsx @@ -2,7 +2,6 @@ import AppHeader from "@saleor/components/AppHeader"; import CardSpacer from "@saleor/components/CardSpacer"; import { ConfirmButtonTransitionState } from "@saleor/components/ConfirmButton"; import Container from "@saleor/components/Container"; -import Form from "@saleor/components/Form"; import Grid from "@saleor/components/Grid"; import { MetadataFormData } from "@saleor/components/Metadata"; import Metadata from "@saleor/components/Metadata/Metadata"; @@ -11,19 +10,9 @@ import SaveButtonBar from "@saleor/components/SaveButtonBar"; import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/ProductErrorWithAttributesFragment"; import { ProductVariant } from "@saleor/fragments/types/ProductVariant"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; -import useFormset, { - FormsetChange, - FormsetData -} from "@saleor/hooks/useFormset"; +import { FormsetData } from "@saleor/hooks/useFormset"; import { VariantUpdate_productVariantUpdate_errors } from "@saleor/products/types/VariantUpdate"; -import { - getAttributeInputFromVariant, - getStockInputFromVariant -} from "@saleor/products/utils/data"; import { ReorderAction } from "@saleor/types"; -import { mapMetadataItemToInput } from "@saleor/utils/maps"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; -import { diff } from "fast-array-diff"; import React from "react"; import { maybe } from "../../../misc"; @@ -37,6 +26,7 @@ import ProductVariantImageSelectDialog from "../ProductVariantImageSelectDialog" import ProductVariantNavigation from "../ProductVariantNavigation"; import ProductVariantPrice from "../ProductVariantPrice"; import ProductVariantSetDefault from "../ProductVariantSetDefault"; +import ProductVariantUpdateForm from "./form"; export interface ProductVariantPageFormData extends MetadataFormData { costPrice: string; @@ -97,77 +87,17 @@ const ProductVariantPage: React.FC = ({ onSetDefaultVariant, onWarehouseConfigure }) => { - const attributeInput = React.useMemo( - () => getAttributeInputFromVariant(variant), - [variant] - ); - const stockInput = React.useMemo(() => getStockInputFromVariant(variant), [ - variant - ]); - const { change: changeAttributeData, data: attributes } = useFormset( - attributeInput - ); - const { - add: addStock, - change: changeStockData, - data: stocks, - remove: removeStock - } = useFormset(stockInput); - const [isModalOpened, setModalStatus] = React.useState(false); const toggleModal = () => setModalStatus(!isModalOpened); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); - - const variantImages = maybe(() => variant.images.map(image => image.id), []); - const productImages = maybe(() => - variant.product.images.sort((prev, next) => + const variantImages = variant?.images?.map(image => image.id) || []; + const productImages = + variant?.product?.images?.sort((prev, next) => prev.sortOrder > next.sortOrder ? 1 : -1 - ) - ); - const images = maybe(() => - productImages - .filter(image => variantImages.indexOf(image.id) !== -1) - .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)) - ); - - const initialForm: ProductVariantPageFormData = { - costPrice: variant?.costPrice?.amount.toString() || "", - metadata: variant?.metadata?.map(mapMetadataItemToInput), - price: variant?.price?.amount.toString() || "", - privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput), - sku: variant?.sku || "", - trackInventory: !!variant?.trackInventory, - weight: variant?.weight?.value.toString() || "" - }; - - const handleSubmit = (data: ProductVariantPageFormData) => { - const dataStocks = stocks.map(stock => stock.id); - const variantStocks = variant.stocks.map(stock => stock.warehouse.id); - const stockDiff = diff(variantStocks, dataStocks); - const metadata = isMetadataModified ? data.metadata : undefined; - const privateMetadata = isPrivateMetadataModified - ? data.privateMetadata - : undefined; - - onSubmit({ - ...data, - addStocks: stocks.filter(stock => - stockDiff.added.some(addedStock => addedStock === stock.id) - ), - attributes, - metadata, - privateMetadata, - removeStocks: stockDiff.removed, - updateStocks: stocks.filter( - stock => !stockDiff.added.some(addedStock => addedStock === stock.id) - ) - }); - }; + ) || []; + const images = productImages + .filter(image => variantImages.indexOf(image.id) !== -1) + .sort((prev, next) => (prev.sortOrder > next.sortOrder ? 1 : -1)); return ( <> @@ -182,116 +112,95 @@ const ProductVariantPage: React.FC = ({ /> )} -
- {({ change, data, hasChanged, submit, triggerChange }) => { - const handleAttributeChange: FormsetChange = (id, value) => { - changeAttributeData(id, value); - triggerChange(); - }; - - const changeMetadata = makeMetadataChangeHandler(change); - - return ( - <> - -
- variant.product.thumbnail.url - )} - variants={maybe(() => variant.product.variants)} - onAdd={onAdd} - onRowClick={(variantId: string) => { - if (variant) { - return onVariantClick(variantId); - } - }} - onReorder={onVariantReorder} - /> -
-
- - - - - + {({ change, data, handlers, hasChanged, submit }) => ( + <> + +
+ variant.product.thumbnail.url + )} + variants={maybe(() => variant.product.variants)} + onAdd={onAdd} + onRowClick={(variantId: string) => { + if (variant) { + return onVariantClick(variantId); } - loading={loading} - onChange={change} - /> - - - - { - triggerChange(); - changeStockData(id, value); - }} - onFormDataChange={change} - onWarehouseStockAdd={id => { - triggerChange(); - addStock({ - data: null, - id, - label: warehouses.find( - warehouse => warehouse.id === id - ).name, - value: "0" - }); - }} - onWarehouseStockDelete={id => { - triggerChange(); - removeStock(id); - }} - onWarehouseConfigure={onWarehouseConfigure} - /> - - -
-
- - - ); - }} - + }} + onReorder={onVariantReorder} + /> +
+
+ + + + + + + + + + + +
+
+ + + )} + {variant && ( ; + stocks: ProductStockInput[]; +} +export interface ProductVariantUpdateSubmitData + extends ProductVariantUpdateFormData { + attributes: FormsetData; + addStocks: ProductStockInput[]; + updateStocks: ProductStockInput[]; + removeStocks: string[]; +} + +export interface UseProductVariantUpdateFormOpts { + warehouses: SearchWarehouses_search_edges_node[]; +} + +export interface UseProductVariantUpdateFormResult { + change: FormChange; + data: ProductVariantUpdateData; + handlers: Record<"changeStock" | "selectAttribute", FormsetChange> & + Record<"addStock" | "deleteStock", (id: string) => void> & { + changeMetadata: FormChange; + }; + hasChanged: boolean; + submit: () => void; +} + +export interface ProductVariantUpdateFormProps + extends UseProductVariantUpdateFormOpts { + children: (props: UseProductVariantUpdateFormResult) => React.ReactNode; + variant: ProductVariant; + onSubmit: (data: ProductVariantUpdateSubmitData) => Promise; +} + +function useProductVariantUpdateForm( + variant: ProductVariant, + onSubmit: (data: ProductVariantUpdateSubmitData) => Promise, + opts: UseProductVariantUpdateFormOpts +): UseProductVariantUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const attributeInput = getAttributeInputFromVariant(variant); + const stockInput = getStockInputFromVariant(variant); + + const initial: ProductVariantUpdateFormData = { + costPrice: variant?.costPrice?.amount.toString() || "", + metadata: variant?.metadata?.map(mapMetadataItemToInput), + price: variant?.price?.amount.toString() || "", + privateMetadata: variant?.privateMetadata?.map(mapMetadataItemToInput), + sku: variant?.sku || "", + trackInventory: variant?.trackInventory, + weight: variant?.weight?.value.toString() || "" + }; + + const form = useForm(initial); + const attributes = useFormset(attributeInput); + const stocks = useFormset(stockInput); + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const changeMetadata = makeMetadataChangeHandler(handleChange); + const handleAttributeChange: FormsetChange = (id, value) => { + attributes.change(id, value); + triggerChange(); + }; + const handleStockAdd = (id: string) => { + triggerChange(); + stocks.add({ + data: null, + id, + label: opts.warehouses.find(warehouse => warehouse.id === id).name, + value: "0" + }); + }; + const handleStockChange = (id: string, value: string) => { + triggerChange(); + stocks.change(id, value); + }; + const handleStockDelete = (id: string) => { + triggerChange(); + stocks.remove(id); + }; + + const metadata = isMetadataModified ? form.data.metadata : undefined; + const privateMetadata = isPrivateMetadataModified + ? form.data.privateMetadata + : undefined; + + const dataStocks = stocks.data.map(stock => stock.id); + const variantStocks = variant?.stocks.map(stock => stock.warehouse.id) || []; + const stockDiff = diff(variantStocks, dataStocks); + + const addStocks = stocks.data.filter(stock => + stockDiff.added.some(addedStock => addedStock === stock.id) + ); + const updateStocks = stocks.data.filter( + stock => !stockDiff.added.some(addedStock => addedStock === stock.id) + ); + + const data: ProductVariantUpdateData = { + ...form.data, + attributes: attributes.data, + stocks: stocks.data + }; + const submitData: ProductVariantUpdateSubmitData = { + ...form.data, + addStocks, + attributes: attributes.data, + metadata, + privateMetadata, + removeStocks: stockDiff.removed, + updateStocks + }; + + const submit = () => handleFormSubmit(submitData, onSubmit, setChanged); + + return { + change: handleChange, + data, + handlers: { + addStock: handleStockAdd, + changeMetadata, + changeStock: handleStockChange, + deleteStock: handleStockDelete, + selectAttribute: handleAttributeChange + }, + hasChanged: changed, + submit + }; +} + +const ProductVariantUpdateForm: React.FC = ({ + children, + variant, + onSubmit, + ...rest +}) => { + const props = useProductVariantUpdateForm(variant, onSubmit, rest); + + return
{children(props)}
; +}; + +ProductVariantUpdateForm.displayName = "ProductVariantUpdateForm"; +export default ProductVariantUpdateForm; diff --git a/src/utils/handlers/handleFormSubmit.ts b/src/utils/handlers/handleFormSubmit.ts index 625dc2381..8211c6548 100644 --- a/src/utils/handlers/handleFormSubmit.ts +++ b/src/utils/handlers/handleFormSubmit.ts @@ -1,9 +1,10 @@ async function handleFormSubmit( data: T, - onSubmit: (data: T) => Promise, + onSubmit: (data: T) => Promise, setChanged: (changed: boolean) => void ): Promise { - const ok = await onSubmit(data); + const errors = await onSubmit(data); + const ok = errors.length === 0; if (ok) { setChanged(false); @@ -11,3 +12,5 @@ async function handleFormSubmit( return ok; } + +export default handleFormSubmit;