feat(admin-ui): Delete pricing forms from edit + create variant modals (#5676)
This commit is contained in:
5
.changeset/khaki-eyes-fetch.md
Normal file
5
.changeset/khaki-eyes-fetch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/admin-ui": patch
|
||||
---
|
||||
|
||||
fix(admin-ui): delete edit variant prices in favor of bulk editor
|
||||
@@ -1,173 +0,0 @@
|
||||
import { useAdminRegions, useAdminStore } from "medusa-react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import NestedPrice from "./nested-price"
|
||||
|
||||
type PricePayload = {
|
||||
id: string | null
|
||||
amount: number | null
|
||||
currency_code: string
|
||||
region_id: string | null
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
type PriceObject = FieldArrayWithId<
|
||||
{
|
||||
__nested__: PricesFormType
|
||||
},
|
||||
"__nested__.prices",
|
||||
"id"
|
||||
> & { index: number }
|
||||
|
||||
export type PricesFormType = {
|
||||
prices: PricePayload[]
|
||||
}
|
||||
|
||||
export type NestedPriceObject = {
|
||||
currencyPrice: PriceObject
|
||||
regionPrices: (PriceObject & { regionName: string })[]
|
||||
}
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<PricesFormType>
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-usable nested form used to submit pricing information for products and their variants.
|
||||
* Fetches store currencies and regions from the backend, and allows the user to specify both
|
||||
* currency and region specific prices.
|
||||
* @example
|
||||
* <Pricing form={nestedForm(form, "prices")} />
|
||||
*/
|
||||
const PricesForm = ({ form }: Props) => {
|
||||
const { store } = useAdminStore()
|
||||
const { regions } = useAdminRegions()
|
||||
|
||||
const { control, path } = form
|
||||
|
||||
const { append, update, fields } = useFieldArray({
|
||||
control,
|
||||
name: path("prices"),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!regions || !store || !fields) {
|
||||
return
|
||||
}
|
||||
|
||||
regions.forEach((reg) => {
|
||||
if (!fields.some((field) => field.region_id === reg.id)) {
|
||||
append({
|
||||
id: null,
|
||||
region_id: reg.id,
|
||||
amount: null,
|
||||
currency_code: reg.currency_code,
|
||||
includes_tax: reg.includes_tax,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
store.currencies.forEach((cur) => {
|
||||
if (!fields.some((field) => field.currency_code === cur.code)) {
|
||||
append({
|
||||
id: null,
|
||||
currency_code: cur.code,
|
||||
amount: null,
|
||||
region_id: null,
|
||||
includes_tax: cur.includes_tax,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [regions, store, fields])
|
||||
|
||||
// Ensure that prices are up to date with their respective tax inclusion setting
|
||||
useEffect(() => {
|
||||
if (!regions || !fields || !store) {
|
||||
return
|
||||
}
|
||||
|
||||
regions.forEach((reg) => {
|
||||
const regionPrice = fields.findIndex(
|
||||
(field) => !!field && field.region_id === reg.id
|
||||
)
|
||||
|
||||
if (
|
||||
regionPrice !== -1 &&
|
||||
fields[regionPrice].includes_tax !== reg.includes_tax
|
||||
) {
|
||||
update(regionPrice, {
|
||||
...fields[regionPrice],
|
||||
includes_tax: reg.includes_tax,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
store.currencies.forEach((cur) => {
|
||||
const currencyPrice = fields.findIndex(
|
||||
(field) =>
|
||||
!!field && !field.region_id && field.currency_code === cur.code
|
||||
)
|
||||
|
||||
if (
|
||||
currencyPrice !== -1 &&
|
||||
fields[currencyPrice].includes_tax !== cur.includes_tax
|
||||
) {
|
||||
update(currencyPrice, {
|
||||
...fields[currencyPrice],
|
||||
includes_tax: cur.includes_tax,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [regions, store])
|
||||
|
||||
const priceObj = useMemo(() => {
|
||||
const obj: Record<string, NestedPriceObject> = {}
|
||||
|
||||
const currencyPrices = fields.filter((field) => field.region_id === null)
|
||||
const regionPrices = fields.filter((field) => field.region_id !== null)
|
||||
|
||||
currencyPrices.forEach((price) => {
|
||||
obj[price.currency_code!] = {
|
||||
currencyPrice: {
|
||||
...price,
|
||||
index: fields.indexOf(price),
|
||||
},
|
||||
regionPrices: regionPrices
|
||||
.filter(
|
||||
(regionPrice) => regionPrice.currency_code === price.currency_code
|
||||
)
|
||||
.map((rp) => ({
|
||||
...rp,
|
||||
regionName: regions?.find((r) => r.id === rp.region_id)?.name || "",
|
||||
index: fields.indexOf(rp),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
return obj
|
||||
}, [fields, regions])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{Object.values(priceObj).map((po) => {
|
||||
return (
|
||||
<NestedPrice
|
||||
form={form}
|
||||
nestedPrice={po}
|
||||
key={po.currencyPrice.id}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PricesForm
|
||||
@@ -1,123 +0,0 @@
|
||||
import { NestedPriceObject, PricesFormType } from "."
|
||||
|
||||
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
|
||||
import { Controller } from "react-hook-form"
|
||||
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
|
||||
import MapPinIcon from "../../../fundamentals/icons/map-pin-icon"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import PriceFormInput from "./price-form-input"
|
||||
import TriangleRightIcon from "../../../fundamentals/icons/triangle-right-icon"
|
||||
import clsx from "clsx"
|
||||
import { currencies } from "../../../../utils/currencies"
|
||||
import useToggleState from "../../../../hooks/use-toggle-state"
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<PricesFormType>
|
||||
nestedPrice: NestedPriceObject
|
||||
}
|
||||
|
||||
const NestedPrice = ({ form, nestedPrice }: Props) => {
|
||||
const { state, toggle } = useToggleState()
|
||||
|
||||
const { control, path } = form
|
||||
const { currencyPrice, regionPrices } = nestedPrice
|
||||
return (
|
||||
<div className="gap-y-2xsmall flex flex-col">
|
||||
<div className="gap-x-base p-2xsmall hover:bg-grey-5 focus-within:bg-grey-5 rounded-rounded relative grid grid-cols-[1fr_223px] justify-between pl-10 transition-colors">
|
||||
<button
|
||||
className={clsx(
|
||||
"left-xsmall text-grey-40 absolute top-1/2 -translate-y-1/2 transition-all",
|
||||
{
|
||||
"rotate-90": state,
|
||||
},
|
||||
{
|
||||
hidden: regionPrices.length === 0,
|
||||
}
|
||||
)}
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
disabled={regionPrices.length === 0}
|
||||
>
|
||||
<TriangleRightIcon />
|
||||
</button>
|
||||
<div className="gap-x-small flex items-center">
|
||||
<div className="bg-grey-10 rounded-rounded text-grey-50 flex h-10 w-10 items-center justify-center">
|
||||
<CoinsIcon size={20} />
|
||||
</div>
|
||||
<div className="gap-x-xsmall flex items-center">
|
||||
<span className="inter-base-semibold">
|
||||
{currencyPrice.currency_code.toUpperCase()}
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50">
|
||||
{currencies[currencyPrice.currency_code.toUpperCase()].name}
|
||||
</span>
|
||||
<IncludesTaxTooltip includesTax={currencyPrice?.includes_tax} />
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name={path(`prices.${currencyPrice.index}.amount`)}
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, formState: { errors } }) => {
|
||||
return (
|
||||
<PriceFormInput
|
||||
onChange={onChange}
|
||||
amount={value !== null ? value : undefined}
|
||||
currencyCode={currencyPrice.currency_code}
|
||||
errors={errors}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ul
|
||||
className={clsx(
|
||||
"gap-y-2xsmall my-2xsmall flex flex-col overflow-hidden",
|
||||
{
|
||||
"max-h-0": !state,
|
||||
"max-h-[9999px]": state,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{regionPrices.map((rp) => {
|
||||
return (
|
||||
<div
|
||||
className="p-2xsmall hover:bg-grey-5 focus-within:bg-grey-5 rounded-rounded grid grid-cols-[1fr_223px] justify-between pl-10 transition-colors"
|
||||
key={rp.id}
|
||||
>
|
||||
<div className="gap-x-small flex items-center">
|
||||
<div className="bg-grey-10 rounded-rounded text-grey-50 flex h-10 w-10 items-center justify-center">
|
||||
<MapPinIcon size={20} />
|
||||
</div>
|
||||
<div className="gap-x-xsmall flex items-center">
|
||||
<span className="inter-base-regular text-grey-50">
|
||||
{rp.regionName}
|
||||
</span>
|
||||
<IncludesTaxTooltip includesTax={rp.includes_tax} />
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name={path(`prices.${rp.index}.amount`)}
|
||||
control={control}
|
||||
render={({
|
||||
field: { value, onChange },
|
||||
formState: { errors },
|
||||
}) => {
|
||||
return (
|
||||
<PriceFormInput
|
||||
onChange={onChange}
|
||||
amount={value !== null ? value : undefined}
|
||||
currencyCode={currencyPrice.currency_code}
|
||||
errors={errors}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NestedPrice
|
||||
@@ -1,28 +1,26 @@
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import InputError from "../../../../atoms/input-error"
|
||||
import IconTooltip from "../../../../molecules/icon-tooltip"
|
||||
import Accordion from "../../../../organisms/accordion"
|
||||
import { PricesFormType } from "../../../general/prices-form"
|
||||
import CustomsForm, { CustomsFormType } from "../../customs-form"
|
||||
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
|
||||
import VariantGeneralForm, {
|
||||
VariantGeneralFormType,
|
||||
} from "../variant-general-form"
|
||||
import VariantPricesForm from "../variant-prices-form"
|
||||
import VariantSelectOptionsForm, {
|
||||
VariantOptionValueType,
|
||||
VariantSelectOptionsFormType,
|
||||
} from "../variant-select-options-form"
|
||||
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
|
||||
|
||||
import Accordion from "../../../../organisms/accordion"
|
||||
import IconTooltip from "../../../../molecules/icon-tooltip"
|
||||
import InputError from "../../../../atoms/input-error"
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
|
||||
export type CreateFlowVariantFormType = {
|
||||
/**
|
||||
* Used to identify the variant during product create flow. Will not be submitted to the backend.
|
||||
*/
|
||||
_internal_id?: string
|
||||
general: VariantGeneralFormType
|
||||
prices: PricesFormType
|
||||
stock: VariantStockFormType
|
||||
options: VariantSelectOptionsFormType
|
||||
customs: CustomsFormType
|
||||
@@ -77,9 +75,6 @@ const CreateFlowVariantForm = ({ form, options, onCreateOption }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item title="Pricing" value="pricing">
|
||||
<VariantPricesForm form={nestedForm(form, "prices")} />
|
||||
</Accordion.Item>
|
||||
<Accordion.Item title="Stock & Inventory" value="stock">
|
||||
<VariantStockForm form={nestedForm(form, "stock")} />
|
||||
</Accordion.Item>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import CustomsForm, { CustomsFormType } from "../../customs-form"
|
||||
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
|
||||
import MetadataForm, { MetadataFormType } from "../../../general/metadata-form"
|
||||
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||
import VariantGeneralForm, {
|
||||
VariantGeneralFormType,
|
||||
} from "../variant-general-form"
|
||||
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
|
||||
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import Accordion from "../../../../organisms/accordion"
|
||||
import IconTooltip from "../../../../molecules/icon-tooltip"
|
||||
import InputField from "../../../../molecules/input"
|
||||
import Accordion from "../../../../organisms/accordion"
|
||||
import MetadataForm, { MetadataFormType } from "../../../general/metadata-form"
|
||||
import { PricesFormType } from "../../../general/prices-form"
|
||||
import VariantPricesForm from "../variant-prices-form"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
|
||||
export type EditFlowVariantFormType = {
|
||||
/**
|
||||
@@ -97,9 +96,6 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item title="Pricing" value="pricing">
|
||||
<VariantPricesForm form={nestedForm(form, "prices")} />
|
||||
</Accordion.Item>
|
||||
{showStockAndInventory && (
|
||||
<Accordion.Item title="Stock & Inventory" value="stock">
|
||||
<VariantStockForm form={nestedForm(form, "stock")} />
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import PricesForm, { PricesFormType } from "../../../general/prices-form"
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<PricesFormType>
|
||||
}
|
||||
|
||||
const VariantPricesForm = ({ form }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<p className="inter-base-regular text-grey-50">
|
||||
Configure the pricing for this variant.
|
||||
</p>
|
||||
<div className="pt-large">
|
||||
<PricesForm form={form} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariantPricesForm
|
||||
@@ -1,27 +1,26 @@
|
||||
import { MoneyAmount, Product } from "@medusajs/client-types"
|
||||
import mapKeys from "lodash/mapKeys"
|
||||
import pick from "lodash/pick"
|
||||
import pickBy from "lodash/pickBy"
|
||||
import { useAdminRegions, useAdminUpdateVariant } from "medusa-react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { currencies as CURRENCY_MAP } from "../../../../utils/currencies"
|
||||
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
import Fade from "../../../atoms/fade-wrapper"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import CrossIcon from "../../../fundamentals/icons/cross-icon"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import DeletePrompt from "../../delete-prompt"
|
||||
import EditPricesActions from "./edit-prices-actions"
|
||||
import EditPricesTable from "./edit-prices-table"
|
||||
import SavePrompt from "./save-prompt"
|
||||
import {
|
||||
getAllProductPricesCurrencies,
|
||||
getAllProductPricesRegions,
|
||||
getCurrencyPricesOnly,
|
||||
getRegionPricesOnly,
|
||||
} from "./utils"
|
||||
import { useAdminRegions, useAdminUpdateVariant } from "medusa-react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import Button from "../../../fundamentals/button"
|
||||
import { currencies as CURRENCY_MAP } from "../../../../utils/currencies"
|
||||
import CrossIcon from "../../../fundamentals/icons/cross-icon"
|
||||
import DeletePrompt from "../../delete-prompt"
|
||||
import EditPricesActions from "./edit-prices-actions"
|
||||
import EditPricesTable from "./edit-prices-table"
|
||||
import Fade from "../../../atoms/fade-wrapper"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import SavePrompt from "./save-prompt"
|
||||
import mapKeys from "lodash/mapKeys"
|
||||
import pick from "lodash/pick"
|
||||
import pickBy from "lodash/pickBy"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
|
||||
type EditPricesModalProps = {
|
||||
close: () => void
|
||||
|
||||
Reference in New Issue
Block a user