feat(admin-ui): Delete pricing forms from edit + create variant modals (#5676)

This commit is contained in:
Philip Korsholm
2023-11-23 15:31:32 +00:00
committed by GitHub
parent a39ce125cc
commit 02ea9ac3ac
7 changed files with 32 additions and 354 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/admin-ui": patch
---
fix(admin-ui): delete edit variant prices in favor of bulk editor

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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")} />

View File

@@ -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

View File

@@ -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