import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa" import { useAdminCreateProduct, useMedusa } from "medusa-react" import { useForm, useWatch } from "react-hook-form" import CustomsForm, { CustomsFormType, } from "../../../components/forms/product/customs-form" import DimensionsForm, { DimensionsFormType, } from "../../../components/forms/product/dimensions-form" import DiscountableForm, { DiscountableFormType, } from "../../../components/forms/product/discountable-form" import GeneralForm, { GeneralFormType, } from "../../../components/forms/product/general-form" import MediaForm, { MediaFormType, } from "../../../components/forms/product/media-form" import OrganizeForm, { OrganizeFormType, } from "../../../components/forms/product/organize-form" import ThumbnailForm, { ThumbnailFormType, } from "../../../components/forms/product/thumbnail-form" import { FormImage, ProductStatus } from "../../../types/shared" import AddSalesChannelsForm, { AddSalesChannelsFormType, } from "./add-sales-channels" import AddVariantsForm, { AddVariantsFormType } from "./add-variants" import { useEffect } from "react" import { useNavigate } from "react-router-dom" import { useTranslation } from "react-i18next" import { PricesFormType } from "../../../components/forms/general/prices-form" import Button from "../../../components/fundamentals/button" import FeatureToggle from "../../../components/fundamentals/feature-toggle" import CrossIcon from "../../../components/fundamentals/icons/cross-icon" import FocusModal from "../../../components/molecules/modal/focus-modal" import Accordion from "../../../components/organisms/accordion" import useNotification from "../../../hooks/use-notification" import { useFeatureFlag } from "../../../providers/feature-flag-provider" import { getErrorMessage } from "../../../utils/error-messages" import { prepareImages } from "../../../utils/images" import { nestedForm } from "../../../utils/nested-form" type NewProductForm = { general: GeneralFormType discounted: DiscountableFormType organize: OrganizeFormType variants: AddVariantsFormType customs: CustomsFormType dimensions: DimensionsFormType thumbnail: ThumbnailFormType media: MediaFormType salesChannels: AddSalesChannelsFormType } type Props = { onClose: () => void } const NewProduct = ({ onClose }: Props) => { const { t } = useTranslation() const form = useForm({ defaultValues: createBlank(), }) const { mutate } = useAdminCreateProduct() const navigate = useNavigate() const notification = useNotification() const watchedCustoms = useWatch({ control: form.control, name: "customs", }) const watchedDimensions = useWatch({ control: form.control, name: "dimensions", }) const { handleSubmit, formState: { isDirty }, reset, } = form const closeAndReset = () => { reset(createBlank()) onClose() } useEffect(() => { reset(createBlank()) }, []) const { isFeatureEnabled } = useFeatureFlag() const onSubmit = (publish = true) => handleSubmit(async (data) => { const optionsToStockLocationsMap = new Map( data.variants.entries.map((variant) => { return [ variant.options .map(({ option }) => option?.value || "") .sort() .join(","), variant.stock.stock_location, ] }) ) const payload = createPayload( data, publish, isFeatureEnabled("sales_channels") ) if (data.media?.images?.length) { let preppedImages: FormImage[] = [] try { preppedImages = await prepareImages(data.media.images) } catch (error) { let errorMessage = t( "new-something-went-wrong-while-trying-to-upload-images", "Something went wrong while trying to upload images." ) const response = (error as any).response as Response if (response.status === 500) { errorMessage = errorMessage + " " + t( "new-no-file-service-configured", "You might not have a file service configured. Please contact your administrator" ) } notification(t("new-error", "Error"), errorMessage, "error") return } const urls = preppedImages.map((image) => image.url) payload.images = urls } if (data.thumbnail?.images?.length) { let preppedImages: FormImage[] = [] try { preppedImages = await prepareImages(data.thumbnail.images) } catch (error) { let errorMessage = t( "new-upload-thumbnail-error", "Something went wrong while trying to upload the thumbnail." ) const response = (error as any).response as Response if (response.status === 500) { errorMessage = errorMessage + " " + t( "new-no-file-service-configured", "You might not have a file service configured. Please contact your administrator" ) } notification(t("new-error", "Error"), errorMessage, "error") return } const urls = preppedImages.map((image) => image.url) payload.thumbnail = urls[0] } mutate(payload, { onSuccess: ({ product }) => { createStockLocationsForVariants( product.variants, optionsToStockLocationsMap ).then(() => { closeAndReset() navigate(`/a/products/${product.id}`) }) }, onError: (err) => { notification(t("new-error", "Error"), getErrorMessage(err), "error") }, }) }) const { client } = useMedusa() const createStockLocationsForVariants = async ( variants: ProductVariant[], stockLocationsMap: Map< string, { stocked_quantity: number; location_id: string }[] | undefined > ) => { await Promise.all( variants .map(async (variant) => { const optionsKey = variant.options .map((option) => option?.value || "") .sort() .join(",") const stock_locations = stockLocationsMap.get(optionsKey) if (!stock_locations?.length) { return } const inventory = await client.admin.variants.getInventory(variant.id) return await Promise.all( inventory.variant.inventory .map(async (item) => { return Promise.all( stock_locations.map(async (stock_location) => { client.admin.inventoryItems.createLocationLevel(item.id!, { location_id: stock_location.location_id, stocked_quantity: stock_location.stocked_quantity, }) }) ) }) .flat() ) }) .flat() ) } return (

{t( "new-to-start-selling-all-you-need-is-a-name-and-a-price", "To start selling, all you need is a name and a price." )}

{t( "new-to-start-selling-all-you-need-is-a-name-and-a-price", "To start selling, all you need is a name and a price." )}

{t("new-organize-product", "Organize Product")}

{t( "new-add-variations-of-this-product", "Add variations of this product." )}
{t( "new-offer-your-customers-different-options-for-color-format-size-shape-etc", "Offer your customers different options for color, format, size, shape, etc." )}

{t( "new-used-for-shipping-and-customs-purposes", "Used for shipping and customs purposes." )}

{t("new-dimensions", "Dimensions")}

{t("new-customs", "Customs")}

{t( "new-used-to-represent-your-product-during-checkout-social-sharing-and-more", "Used to represent your product during checkout, social sharing and more." )}

{t( "new-add-images-to-your-product", "Add images to your product." )}

) } const createPayload = ( data: NewProductForm, publish = true, salesChannelsEnabled = false ): AdminPostProductsReq => { const payload: AdminPostProductsReq = { title: data.general.title, subtitle: data.general.subtitle || undefined, material: data.general.material || undefined, handle: data.general.handle, discountable: data.discounted.value, is_giftcard: false, collection_id: data.organize.collection?.value, description: data.general.description || undefined, height: data.dimensions.height || undefined, length: data.dimensions.length || undefined, weight: data.dimensions.weight || undefined, width: data.dimensions.width || undefined, hs_code: data.customs.hs_code || undefined, mid_code: data.customs.mid_code || undefined, type: data.organize.type ? { value: data.organize.type.label, id: data.organize.type.value, } : undefined, tags: data.organize.tags ? data.organize.tags.map((t) => ({ value: t, })) : undefined, categories: data.organize.categories?.length ? data.organize.categories.map((id) => ({ id })) : undefined, origin_country: data.customs.origin_country?.value || undefined, options: data.variants.options.map((o) => ({ title: o.title, })), variants: data.variants.entries.map((v) => ({ title: v.general.title!, material: v.general.material || undefined, inventory_quantity: v.stock.inventory_quantity || 0, prices: getVariantPrices(v.prices), allow_backorder: v.stock.allow_backorder, sku: v.stock.sku || undefined, barcode: v.stock.barcode || undefined, options: v.options.map((o) => ({ value: o.option?.value!, })), ean: v.stock.ean || undefined, upc: v.stock.upc || undefined, height: v.dimensions.height || undefined, length: v.dimensions.length || undefined, weight: v.dimensions.weight || undefined, width: v.dimensions.width || undefined, hs_code: v.customs.hs_code || undefined, mid_code: v.customs.mid_code || undefined, origin_country: v.customs.origin_country?.value || undefined, manage_inventory: v.stock.manage_inventory, })), // @ts-ignore status: publish ? ProductStatus.PUBLISHED : ProductStatus.DRAFT, } if (salesChannelsEnabled) { payload.sales_channels = data.salesChannels.channels.map((c) => ({ id: c.id, })) } return payload } const createBlank = (): NewProductForm => { return { general: { title: "", material: null, subtitle: null, description: null, handle: "", }, customs: { hs_code: null, mid_code: null, origin_country: null, }, dimensions: { height: null, length: null, weight: null, width: null, }, discounted: { value: true, }, media: { images: [], }, organize: { categories: null, collection: null, tags: null, type: null, }, salesChannels: { channels: [], }, thumbnail: { images: [], }, variants: { entries: [], options: [], }, } } const getVariantPrices = (prices: PricesFormType) => { const priceArray = prices.prices .filter((price) => typeof price.amount === "number") .map((price) => { return { amount: price.amount as number, currency_code: price.region_id ? undefined : price.currency_code, region_id: price.region_id || undefined, } }) return priceArray } export default NewProduct