From 374a072bf75ccd9cc003ad16a87c26e283511c53 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Tue, 20 Oct 2020 16:44:21 +0200 Subject: [PATCH] Extract product update form to separate component --- .../ProductUpdatePage/ProductUpdatePage.tsx | 513 +++++++----------- .../components/ProductUpdatePage/form.tsx | 259 +++++++++ 2 files changed, 454 insertions(+), 318 deletions(-) create mode 100644 src/products/components/ProductUpdatePage/form.tsx diff --git a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx index 623da6239..9a6a6cf77 100644 --- a/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx +++ b/src/products/components/ProductUpdatePage/ProductUpdatePage.tsx @@ -3,7 +3,6 @@ import AvailabilityCard from "@saleor/components/AvailabilityCard"; 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 Metadata from "@saleor/components/Metadata/Metadata"; import PageHeader from "@saleor/components/PageHeader"; @@ -13,18 +12,13 @@ import { ProductErrorWithAttributesFragment } from "@saleor/fragments/types/Prod import { TaxTypeFragment } from "@saleor/fragments/types/TaxTypeFragment"; import { WarehouseFragment } from "@saleor/fragments/types/WarehouseFragment"; import useDateLocalize from "@saleor/hooks/useDateLocalize"; -import useFormset from "@saleor/hooks/useFormset"; import useStateFromProps from "@saleor/hooks/useStateFromProps"; import { sectionNames } from "@saleor/intl"; import { maybe } from "@saleor/misc"; import { SearchCategories_search_edges_node } from "@saleor/searches/types/SearchCategories"; import { SearchCollections_search_edges_node } from "@saleor/searches/types/SearchCollections"; import { FetchMoreProps, ListActions, ReorderAction } from "@saleor/types"; -import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; -import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; -import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; import { convertFromRaw, RawDraftContentState } from "draft-js"; -import { diff } from "fast-array-diff"; import React from "react"; import { useIntl } from "react-intl"; @@ -33,17 +27,7 @@ import { ProductDetails_product_images, ProductDetails_product_variants } from "../../types/ProductDetails"; -import { - getAttributeInputFromProduct, - getChoices, - getProductUpdatePageFormData, - getStockInputFromProduct, - ProductUpdatePageFormData -} from "../../utils/data"; -import { - createAttributeChangeHandler, - createAttributeMultiChangeHandler -} from "../../utils/handlers"; +import { getChoices, ProductUpdatePageFormData } from "../../utils/data"; import ProductAttributes, { ProductAttributeInput } from "../ProductAttributes"; import ProductDetailsForm from "../ProductDetailsForm"; import ProductImages from "../ProductImages"; @@ -53,6 +37,7 @@ import ProductShipping from "../ProductShipping/ProductShipping"; import ProductStocks, { ProductStockInput } from "../ProductStocks"; import ProductTaxes from "../ProductTaxes"; import ProductVariants from "../ProductVariants"; +import ProductUpdateForm from "./form"; export interface ProductUpdatePageProps extends ListActions { defaultWeightUnit: string; @@ -136,25 +121,9 @@ export const ProductUpdatePage: React.FC = ({ }) => { const intl = useIntl(); const localizeDate = useDateLocalize(); - const attributeInput = React.useMemo( - () => getAttributeInputFromProduct(product), - [product] - ); - const stockInput = React.useMemo(() => getStockInputFromProduct(product), [ - product - ]); - const { change: changeAttributeData, data: attributes } = useFormset( - attributeInput - ); - const { - add: addStock, - change: changeStockData, - data: stocks, - remove: removeStock - } = useFormset(stockInput); const [selectedCategory, setSelectedCategory] = useStateFromProps( - maybe(() => product.category.name, "") + product?.category?.name || "" ); const [selectedCollections, setSelectedCollections] = useStateFromProps( @@ -165,309 +134,217 @@ export const ProductUpdatePage: React.FC = ({ product?.taxType.description ); - const { - isMetadataModified, - isPrivateMetadataModified, - makeChangeHandler: makeMetadataChangeHandler - } = useMetadataChangeTrigger(); - - const initialData = getProductUpdatePageFormData(product, variants); const initialDescription = maybe(() => JSON.parse(product.descriptionJson) ); const categories = getChoices(categoryChoiceList); const collections = getChoices(collectionChoiceList); - const currency = - product?.variants?.length && product.variants[0].price.currency; - const hasVariants = maybe(() => product.productType.hasVariants, false); + const currency = product?.variants[0]?.price.currency; + const hasVariants = product?.productType?.hasVariants; const taxTypeChoices = taxTypes?.map(taxType => ({ label: taxType.description, value: taxType.taxCode })) || []; - const getAvailabilityData = ({ - availableForPurchase, - isAvailableForPurchase, - isPublished, - publicationDate - }: ProductUpdatePageFormData) => ({ - isAvailableForPurchase: isAvailableForPurchase || !!availableForPurchase, - isPublished: isPublished || !!publicationDate - }); - - const getStocksData = () => { - if (product.productType.hasVariants) { - return { addStocks: [], removeStocks: [], updateStocks: [] }; - } - - const dataStocks = stocks.map(stock => stock.id); - const variantStocks = product.variants[0]?.stocks.map( - stock => stock.warehouse.id - ); - const stockDiff = diff(variantStocks, dataStocks); - - return { - addStocks: stocks.filter(stock => - stockDiff.added.some(addedStock => addedStock === stock.id) - ), - removeStocks: stockDiff.removed, - updateStocks: stocks.filter( - stock => !stockDiff.added.some(addedStock => addedStock === stock.id) - ) - }; - }; - - const getMetadata = (data: ProductUpdatePageFormData) => ({ - metadata: isMetadataModified ? data.metadata : undefined, - privateMetadata: isPrivateMetadataModified - ? data.privateMetadata - : undefined - }); - - const getParsedData = (data: ProductUpdatePageFormData) => ({ - ...data, - ...getAvailabilityData(data), - ...getStocksData(), - ...getMetadata(data), - attributes - }); - - const handleSubmit = (data: ProductUpdatePageFormData) => - onSubmit(getParsedData(data)); - return ( -
- {({ change, data, hasChanged, submit, triggerChange, toggleValue }) => { - const handleCollectionSelect = createMultiAutocompleteSelectHandler( - toggleValue, - setSelectedCollections, - selectedCollections, - collections - ); - const handleCategorySelect = createSingleAutocompleteSelectHandler( - change, - setSelectedCategory, - categories - ); - const handleAttributeChange = createAttributeChangeHandler( - changeAttributeData, - triggerChange - ); - const handleAttributeMultiChange = createAttributeMultiChangeHandler( - changeAttributeData, - attributes, - triggerChange - ); - const changeMetadata = makeMetadataChangeHandler(change); - const handleTaxTypeSelect = createSingleAutocompleteSelectHandler( - change, - setSelectedTaxType, - taxTypeChoices - ); - - return ( - <> - - - {intl.formatMessage(sectionNames.products)} - - - -
- + {({ change, data, handlers, hasChanged, submit }) => ( + <> + + + {intl.formatMessage(sectionNames.products)} + + + +
+ + + + + {data.attributes.length > 0 && ( + - - - - {attributes.length > 0 && ( - + {!!product?.productType && !hasVariants && ( + <> + - )} - - {!!product?.productType && !hasVariants && ( - <> - - - - )} - {hasVariants ? ( - + + )} + {hasVariants ? ( + + ) : ( + <> + + + + + )} + + + convertFromRaw(data.description) + .getPlainText() + .slice(0, 300) + )} + slug={data.slug} + slugPlaceholder={data.name} + loading={disabled} + onClick={onSeoClick} + onChange={change} + helperText={intl.formatMessage({ + defaultMessage: + "Add search engine title and description to make this product easier to find" + })} + /> + + +
+
+ + + - ) : ( - <> - - - { - 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} - /> - - )} - - - convertFromRaw(data.description) - .getPlainText() - .slice(0, 300) - )} - slug={data.slug} - slugPlaceholder={data.name} - loading={disabled} - onClick={onSeoClick} - onChange={change} - helperText={intl.formatMessage({ - defaultMessage: - "Add search engine title and description to make this product easier to find" - })} - /> - - -
-
- product.productType)} - onCategoryChange={handleCategorySelect} - onCollectionChange={handleCollectionSelect} - /> - - - - -
-
- -
- - ); - }} - + ), + visibleLabel: intl.formatMessage({ + defaultMessage: "Published", + description: "product label" + }) + }} + onChange={change} + /> + + +
+
+ +
+ + )} + ); }; ProductUpdatePage.displayName = "ProductUpdatePage"; diff --git a/src/products/components/ProductUpdatePage/form.tsx b/src/products/components/ProductUpdatePage/form.tsx new file mode 100644 index 000000000..f4a682e55 --- /dev/null +++ b/src/products/components/ProductUpdatePage/form.tsx @@ -0,0 +1,259 @@ +import { MetadataFormData } from "@saleor/components/Metadata"; +import { MultiAutocompleteChoiceType } from "@saleor/components/MultiAutocompleteSelectField"; +import { SingleAutocompleteChoiceType } from "@saleor/components/SingleAutocompleteSelectField"; +import useForm, { FormChange } from "@saleor/hooks/useForm"; +import useFormset, { + FormsetChange, + FormsetData +} from "@saleor/hooks/useFormset"; +import { ProductDetails_product } from "@saleor/products/types/ProductDetails"; +import { + getAttributeInputFromProduct, + getProductUpdatePageFormData, + getStockInputFromProduct +} from "@saleor/products/utils/data"; +import { + createAttributeChangeHandler, + createAttributeMultiChangeHandler +} from "@saleor/products/utils/handlers"; +import { SearchWarehouses_search_edges_node } from "@saleor/searches/types/SearchWarehouses"; +import createMultiAutocompleteSelectHandler from "@saleor/utils/handlers/multiAutocompleteSelectChangeHandler"; +import createSingleAutocompleteSelectHandler from "@saleor/utils/handlers/singleAutocompleteSelectChangeHandler"; +import useMetadataChangeTrigger from "@saleor/utils/metadata/useMetadataChangeTrigger"; +import { RawDraftContentState } from "draft-js"; +import { diff } from "fast-array-diff"; +import React from "react"; + +import { ProductAttributeInput } from "../ProductAttributes"; +import { ProductStockInput } from "../ProductStocks"; + +export interface ProductUpdateFormData extends MetadataFormData { + availableForPurchase: string; + basePrice: number; + category: string | null; + changeTaxCode: boolean; + chargeTaxes: boolean; + collections: string[]; + description: RawDraftContentState; + isAvailable: boolean; + isAvailableForPurchase: boolean; + isPublished: boolean; + name: string; + slug: string; + publicationDate: string; + seoDescription: string; + seoTitle: string; + sku: string; + taxCode: string; + trackInventory: boolean; + visibleInListings: boolean; + weight: string; +} +export interface ProductUpdateData extends ProductUpdateFormData { + attributes: ProductAttributeInput[]; + stocks: ProductStockInput[]; +} +export interface ProductUpdateSubmitData extends ProductUpdateFormData { + attributes: ProductAttributeInput[]; + collections: string[]; + addStocks: ProductStockInput[]; + updateStocks: ProductStockInput[]; + removeStocks: string[]; +} + +export interface UseProductUpdateFormResult { + change: FormChange; + data: ProductUpdateData; + handlers: Record< + "changeMetadata" | "selectCategory" | "selectCollection" | "selectTaxRate", + FormChange + > & + Record< + "changeStock" | "selectAttribute" | "selectAttributeMultiple", + FormsetChange + > & + Record<"addStock" | "deleteStock", (id: string) => void>; + hasChanged: boolean; + submit: () => Promise; +} + +export interface UseProductUpdateFormOpts + extends Record< + "categories" | "collections" | "taxTypes", + SingleAutocompleteChoiceType[] + > { + setSelectedCategory: React.Dispatch>; + setSelectedCollections: React.Dispatch< + React.SetStateAction + >; + setSelectedTaxType: React.Dispatch>; + selectedCollections: MultiAutocompleteChoiceType[]; + warehouses: SearchWarehouses_search_edges_node[]; +} + +export interface ProductUpdateFormProps extends UseProductUpdateFormOpts { + children: (props: UseProductUpdateFormResult) => React.ReactNode; + product: ProductDetails_product; + onSubmit: (data: ProductUpdateSubmitData) => Promise; +} + +const getAvailabilityData = ({ + availableForPurchase, + isAvailableForPurchase, + isPublished, + publicationDate +}: ProductUpdateFormData) => ({ + isAvailableForPurchase: isAvailableForPurchase || !!availableForPurchase, + isPublished: isPublished || !!publicationDate +}); + +const getStocksData = ( + product: ProductDetails_product, + stocks: FormsetData +) => { + if (product.productType.hasVariants) { + return { addStocks: [], removeStocks: [], updateStocks: [] }; + } + + const dataStocks = stocks.map(stock => stock.id); + const variantStocks = product.variants[0]?.stocks.map( + stock => stock.warehouse.id + ); + const stockDiff = diff(variantStocks, dataStocks); + + return { + addStocks: stocks.filter(stock => + stockDiff.added.some(addedStock => addedStock === stock.id) + ), + removeStocks: stockDiff.removed, + updateStocks: stocks.filter( + stock => !stockDiff.added.some(addedStock => addedStock === stock.id) + ) + }; +}; + +const getMetadata = ( + data: ProductUpdateFormData, + isMetadataModified: boolean, + isPrivateMetadataModified: boolean +) => ({ + metadata: isMetadataModified ? data.metadata : undefined, + privateMetadata: isPrivateMetadataModified ? data.privateMetadata : undefined +}); + +function useProductUpdateForm( + product: ProductDetails_product, + onSubmit: (data: ProductUpdateSubmitData) => Promise, + opts: UseProductUpdateFormOpts +): UseProductUpdateFormResult { + const [changed, setChanged] = React.useState(false); + const triggerChange = () => setChanged(true); + + const form = useForm( + getProductUpdatePageFormData(product, product?.variants) + ); + const attributes = useFormset(getAttributeInputFromProduct(product)); + const stocks = useFormset(getStockInputFromProduct(product)); + + const { + isMetadataModified, + isPrivateMetadataModified, + makeChangeHandler: makeMetadataChangeHandler + } = useMetadataChangeTrigger(); + + const handleChange: FormChange = (event, cb) => { + form.change(event, cb); + triggerChange(); + }; + const handleCollectionSelect = createMultiAutocompleteSelectHandler( + form.toggleValue, + opts.setSelectedCollections, + opts.selectedCollections, + opts.collections + ); + const handleCategorySelect = createSingleAutocompleteSelectHandler( + handleChange, + opts.setSelectedCategory, + opts.categories + ); + const handleAttributeChange = createAttributeChangeHandler( + attributes.change, + triggerChange + ); + const handleAttributeMultiChange = createAttributeMultiChangeHandler( + attributes.change, + attributes.data, + triggerChange + ); + const handleStockChange: FormsetChange = (id, value) => { + triggerChange(); + stocks.change(id, value); + }; + const handleStockAdd = (id: string) => { + triggerChange(); + stocks.add({ + data: null, + id, + label: opts.warehouses.find(warehouse => warehouse.id === id).name, + value: "0" + }); + }; + const handleStockDelete = (id: string) => { + triggerChange(); + stocks.remove(id); + }; + const handleTaxTypeSelect = createSingleAutocompleteSelectHandler( + handleChange, + opts.setSelectedTaxType, + opts.taxTypes + ); + const changeMetadata = makeMetadataChangeHandler(handleChange); + + const data: ProductUpdateData = { + ...form.data, + attributes: attributes.data, + stocks: stocks.data + }; + + const submit = () => + onSubmit({ + ...data, + ...getAvailabilityData(data), + ...getStocksData(product, stocks.data), + ...getMetadata(data, isMetadataModified, isPrivateMetadataModified), + addStocks: [], + attributes: attributes.data + }); + + return { + change: handleChange, + data, + handlers: { + addStock: handleStockAdd, + changeMetadata, + changeStock: handleStockChange, + deleteStock: handleStockDelete, + selectAttribute: handleAttributeChange, + selectAttributeMultiple: handleAttributeMultiChange, + selectCategory: handleCategorySelect, + selectCollection: handleCollectionSelect, + selectTaxRate: handleTaxTypeSelect + }, + hasChanged: changed, + submit + }; +} + +const ProductUpdateForm: React.FC = ({ + children, + product, + onSubmit, + ...rest +}) => { + const props = useProductUpdateForm(product, onSubmit, rest); + + return
{children(props)}
; +}; + +ProductUpdateForm.displayName = "ProductUpdateForm"; +export default ProductUpdateForm;