fix(admin-ui): Gift Card manage page (#3532)
**What** - Updates GC manage page to use product page sections - Revamps Denomination section - Updates the location of several components to reflect that they are now shared between the GC and products domain   Resolves CORE-1089
This commit is contained in:
committed by
GitHub
parent
3171b0e518
commit
bfef22b33e
5
.changeset/neat-lamps-repeat.md
Normal file
5
.changeset/neat-lamps-repeat.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/admin-ui": patch
|
||||
---
|
||||
|
||||
fix(admin-ui): Revamps gift card manage page
|
||||
@@ -1,25 +1,30 @@
|
||||
import * as RadixSwitch from "@radix-ui/react-switch"
|
||||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
|
||||
/**
|
||||
* A controlled switch component atom.
|
||||
*/
|
||||
function Switch(props: RadixSwitch.SwitchProps) {
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
{...props}
|
||||
disabled={props.disabled}
|
||||
className={clsx(
|
||||
"transition-bg radix-state-checked:bg-violet-60 h-[18px] w-8 rounded-full bg-gray-300"
|
||||
)}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
const Switch = React.forwardRef<HTMLButtonElement, RadixSwitch.SwitchProps>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx(
|
||||
"radix-state-checked:translate-x-[19px] block h-2 w-2 translate-x-[5px] rounded-full bg-white transition-transform"
|
||||
"transition-bg radix-state-checked:bg-violet-60 h-[18px] w-8 rounded-full bg-gray-300"
|
||||
)}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
)
|
||||
}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
className={clsx(
|
||||
"radix-state-checked:translate-x-[19px] block h-2 w-2 translate-x-[5px] rounded-full bg-white transition-transform"
|
||||
)}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Switch.displayName = "Switch"
|
||||
|
||||
export default Switch
|
||||
|
||||
@@ -42,7 +42,7 @@ const Tooltip = ({
|
||||
sideOffset={8}
|
||||
align="center"
|
||||
className={clsx(
|
||||
"inter-small-semibold text-grey-50",
|
||||
"inter-small-semibold text-grey-50 z-[999]",
|
||||
"bg-grey-0 shadow-dropdown rounded-rounded py-2 px-3",
|
||||
"border-grey-20 border border-solid",
|
||||
className
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import clsx from "clsx"
|
||||
import { Controller } from "react-hook-form"
|
||||
import { NestedPriceObject, PricesFormType } from "."
|
||||
import IncludesTaxTooltip from "../../../../components/atoms/includes-tax-tooltip"
|
||||
import CoinsIcon from "../../../../components/fundamentals/icons/coins-icon"
|
||||
import MapPinIcon from "../../../../components/fundamentals/icons/map-pin-icon"
|
||||
import TriangleRightIcon from "../../../../components/fundamentals/icons/triangle-right-icon"
|
||||
import useToggleState from "../../../../hooks/use-toggle-state"
|
||||
import { currencies } from "../../../../utils/currencies"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
|
||||
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
|
||||
import MapPinIcon from "../../../fundamentals/icons/map-pin-icon"
|
||||
import TriangleRightIcon from "../../../fundamentals/icons/triangle-right-icon"
|
||||
import PriceFormInput from "./price-form-input"
|
||||
|
||||
type Props = {
|
||||
@@ -21,7 +21,7 @@ const NestedPrice = ({ form, nestedPrice }: Props) => {
|
||||
const { control, path } = form
|
||||
const { currencyPrice, regionPrices } = nestedPrice
|
||||
return (
|
||||
<div key={currencyPrice.id} className="gap-y-2xsmall flex flex-col">
|
||||
<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(
|
||||
@@ -1,8 +1,8 @@
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useState } from "react"
|
||||
import AmountField from "react-currency-input-field"
|
||||
import InputError from "../../../../components/atoms/input-error"
|
||||
import { currencies } from "../../../../utils/currencies"
|
||||
import InputError from "../../../atoms/input-error"
|
||||
|
||||
type Props = {
|
||||
currencyCode: string
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Controller, useFieldArray, useWatch } from "react-hook-form"
|
||||
import { currencies } from "../../../../utils/currencies"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip"
|
||||
import InputError from "../../../atoms/input-error"
|
||||
import Switch from "../../../atoms/switch"
|
||||
import CoinsIcon from "../../../fundamentals/icons/coins-icon"
|
||||
import IconTooltip from "../../../molecules/icon-tooltip"
|
||||
import PriceFormInput from "../../general/prices-form/price-form-input"
|
||||
|
||||
type DenominationType = {
|
||||
amount: number
|
||||
currency_code: string
|
||||
includes_tax?: boolean
|
||||
}
|
||||
|
||||
export type DenominationFormType = {
|
||||
defaultDenomination: DenominationType
|
||||
currencyDenominations: DenominationType[]
|
||||
useSameValue: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<DenominationFormType>
|
||||
}
|
||||
|
||||
const DenominationForm = ({ form }: Props) => {
|
||||
const {
|
||||
control,
|
||||
path,
|
||||
formState: { errors },
|
||||
} = form
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: path("currencyDenominations"),
|
||||
keyName: "fieldKey",
|
||||
})
|
||||
|
||||
const defaultCurrency = useWatch({
|
||||
control,
|
||||
name: path("defaultDenomination"),
|
||||
})
|
||||
|
||||
const useSameValue = useWatch({
|
||||
control,
|
||||
name: path("useSameValue"),
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="gap-y-xlarge flex flex-col">
|
||||
<div>
|
||||
<div className="gap-x-2xsmall flex items-center">
|
||||
<h2 className="inter-large-semibold">Default currency</h2>
|
||||
<IconTooltip
|
||||
type="info"
|
||||
content="The denomination in your store's default currency"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-small gap-x-base rounded-rounded relative grid grid-cols-[1fr_223px] justify-between transition-colors">
|
||||
<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">
|
||||
{defaultCurrency.currency_code.toUpperCase()}
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50">
|
||||
{currencies[defaultCurrency.currency_code.toUpperCase()].name}
|
||||
</span>
|
||||
<IncludesTaxTooltip includesTax={defaultCurrency.includes_tax} />
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name={path(`defaultDenomination.amount`)}
|
||||
control={control}
|
||||
rules={{
|
||||
required:
|
||||
"An amount for your store's default currency is required",
|
||||
}}
|
||||
render={({ field: { value, onChange, name } }) => {
|
||||
return (
|
||||
<>
|
||||
<PriceFormInput
|
||||
onChange={onChange}
|
||||
amount={value !== null ? value : undefined}
|
||||
currencyCode={defaultCurrency.currency_code}
|
||||
errors={errors}
|
||||
/>
|
||||
<InputError errors={errors} name={name} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="gap-y-2xsmall flex flex-col">
|
||||
<h2 className="inter-base-semibold">
|
||||
Use value for all currencies?
|
||||
</h2>
|
||||
<p className="inter-small-regular text-grey-50 max-w-[60%]">
|
||||
If enabled the value used for the store's default currency
|
||||
code will also be applied to all other currencies in your store.
|
||||
</p>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name={path("useSameValue")}
|
||||
render={({ field: { value, onChange, ...rest } }) => {
|
||||
return (
|
||||
<Switch checked={value} onCheckedChange={onChange} {...rest} />
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!useSameValue && (
|
||||
<div>
|
||||
<div className="gap-x-2xsmall flex items-center">
|
||||
<h2 className="inter-base-semibold">Other currencies</h2>
|
||||
<IconTooltip
|
||||
type="info"
|
||||
content="The denomination in your store's other currencies"
|
||||
/>
|
||||
</div>
|
||||
<div className="gap-y-xsmall pt-small flex flex-col">
|
||||
{fields.map((denom, index) => {
|
||||
return (
|
||||
<div
|
||||
key={denom.fieldKey}
|
||||
className="gap-x-base rounded-rounded relative grid grid-cols-[1fr_223px] justify-between transition-colors"
|
||||
>
|
||||
<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">
|
||||
{denom.currency_code.toUpperCase()}
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50">
|
||||
{currencies[denom.currency_code.toUpperCase()].name}
|
||||
</span>
|
||||
<IncludesTaxTooltip includesTax={denom.includes_tax} />
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name={path(`currencyDenominations.${index}.amount`)}
|
||||
control={control}
|
||||
render={({
|
||||
field: { value, onChange },
|
||||
formState: { errors },
|
||||
}) => {
|
||||
return (
|
||||
<PriceFormInput
|
||||
onChange={onChange}
|
||||
amount={value !== null ? value : undefined}
|
||||
currencyCode={denom.currency_code}
|
||||
errors={errors}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DenominationForm
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Controller } from "react-hook-form"
|
||||
import InputField from "../../../../components/molecules/input"
|
||||
import { NextSelect } from "../../../../components/molecules/select/next-select"
|
||||
import { Option } from "../../../../types/shared"
|
||||
import { countries } from "../../../../utils/countries"
|
||||
import FormValidator from "../../../../utils/form-validator"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import InputField from "../../../molecules/input"
|
||||
import { NextSelect } from "../../../molecules/select/next-select"
|
||||
|
||||
export type CustomsFormType = {
|
||||
mid_code: string | null
|
||||
@@ -1,6 +1,6 @@
|
||||
import InputField from "../../../../components/molecules/input"
|
||||
import FormValidator from "../../../../utils/form-validator"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import InputField from "../../../molecules/input"
|
||||
|
||||
export type DimensionsFormType = {
|
||||
length: number | null
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller } from "react-hook-form"
|
||||
import Switch from "../../../../components/atoms/switch"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import Switch from "../../../atoms/switch"
|
||||
|
||||
export type DiscountableFormType = {
|
||||
value: boolean
|
||||
@@ -8,9 +8,10 @@ export type DiscountableFormType = {
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<DiscountableFormType>
|
||||
isGiftCard?: boolean
|
||||
}
|
||||
|
||||
const DiscountableForm = ({ form }: Props) => {
|
||||
const DiscountableForm = ({ form, isGiftCard }: Props) => {
|
||||
const { control, path } = form
|
||||
return (
|
||||
<div>
|
||||
@@ -25,7 +26,8 @@ const DiscountableForm = ({ form }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<p className="inter-base-regular text-grey-50">
|
||||
When unchecked discounts will not be applied to this product.
|
||||
When unchecked discounts will not be applied to this{" "}
|
||||
{isGiftCard ? "gift card" : "product"}.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
import InputField from "../../../../components/molecules/input"
|
||||
import TextArea from "../../../../components/molecules/textarea"
|
||||
import FormValidator from "../../../../utils/form-validator"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import InputField from "../../../molecules/input"
|
||||
import TextArea from "../../../molecules/textarea"
|
||||
|
||||
export type GeneralFormType = {
|
||||
title: string
|
||||
@@ -14,9 +14,10 @@ export type GeneralFormType = {
|
||||
type Props = {
|
||||
form: NestedForm<GeneralFormType>
|
||||
requireHandle?: boolean
|
||||
isGiftCard?: boolean
|
||||
}
|
||||
|
||||
const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
const GeneralForm = ({ form, requireHandle = true, isGiftCard }: Props) => {
|
||||
const {
|
||||
register,
|
||||
path,
|
||||
@@ -28,7 +29,7 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
<div className="gap-x-large mb-small grid grid-cols-2">
|
||||
<InputField
|
||||
label="Title"
|
||||
placeholder="Winter Jacket"
|
||||
placeholder={isGiftCard ? "Gift Card" : "Winter Jacket"}
|
||||
required
|
||||
{...register(path("title"), {
|
||||
required: "Title is required",
|
||||
@@ -50,7 +51,8 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
<p className="inter-base-regular text-grey-50 mb-large">
|
||||
Give your product a short and clear title.
|
||||
Give your {isGiftCard ? "gift card" : "product"} a short and clear
|
||||
title.
|
||||
<br />
|
||||
50-60 characters is the recommended length for search engines.
|
||||
</p>
|
||||
@@ -59,10 +61,12 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
label="Handle"
|
||||
tooltipContent={
|
||||
!requireHandle
|
||||
? "The handle is the part of the URL that identifies the product. If not specified, it will be generated from the title."
|
||||
? `The handle is the part of the URL that identifies the ${
|
||||
isGiftCard ? "gift card" : "product"
|
||||
}. If not specified, it will be generated from the title.`
|
||||
: undefined
|
||||
}
|
||||
placeholder="winter-jacket"
|
||||
placeholder={isGiftCard ? "gift-card" : "winter-jacket"}
|
||||
required={requireHandle}
|
||||
{...register(path("handle"), {
|
||||
required: requireHandle ? "Handle is required" : undefined,
|
||||
@@ -74,7 +78,7 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
/>
|
||||
<InputField
|
||||
label="Material"
|
||||
placeholder="100% cotton"
|
||||
placeholder={isGiftCard ? "Paper" : "100% Cotton"}
|
||||
{...register(path("material"), {
|
||||
minLength: FormValidator.minOneCharRule("Material"),
|
||||
pattern: FormValidator.whiteSpaceRule("Material"),
|
||||
@@ -84,14 +88,17 @@ const GeneralForm = ({ form, requireHandle = true }: Props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
label="Description"
|
||||
placeholder="A warm and cozy jacket..."
|
||||
placeholder={
|
||||
isGiftCard ? "The gift card is..." : "A warm and cozy jacket..."
|
||||
}
|
||||
rows={3}
|
||||
className="mb-small"
|
||||
{...register(path("description"))}
|
||||
errors={errors}
|
||||
/>
|
||||
<p className="inter-base-regular text-grey-50">
|
||||
Give your product a short and clear description.
|
||||
Give your {isGiftCard ? "gift card" : "product"} a short and clear
|
||||
description.
|
||||
<br />
|
||||
120-160 characters is the recommended length for search engines.
|
||||
</p>
|
||||
@@ -6,15 +6,13 @@ import {
|
||||
useFieldArray,
|
||||
useWatch,
|
||||
} from "react-hook-form"
|
||||
import FileUploadField from "../../../../components/atoms/file-upload-field"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import CheckCircleFillIcon from "../../../../components/fundamentals/icons/check-circle-fill-icon"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
import Actionables, {
|
||||
ActionType,
|
||||
} from "../../../../components/molecules/actionables"
|
||||
import { FormImage } from "../../../../types/shared"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import FileUploadField from "../../../atoms/file-upload-field"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import CheckCircleFillIcon from "../../../fundamentals/icons/check-circle-fill-icon"
|
||||
import TrashIcon from "../../../fundamentals/icons/trash-icon"
|
||||
import Actionables, { ActionType } from "../../../molecules/actionables"
|
||||
|
||||
type ImageType = { selected: boolean } & FormImage
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Controller } from "react-hook-form"
|
||||
import NestedMultiselect from "../../../../domain/categories/components/multiselect"
|
||||
import {
|
||||
FeatureFlag,
|
||||
useFeatureFlag,
|
||||
} from "../../../../providers/feature-flag-provider"
|
||||
import { Option } from "../../../../types/shared"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import InputHeader from "../../../fundamentals/input-header"
|
||||
import {
|
||||
NextCreateableSelect,
|
||||
NextSelect,
|
||||
} from "../../../../components/molecules/select/next-select"
|
||||
import TagInput from "../../../../components/molecules/tag-input"
|
||||
import { Option } from "../../../../types/shared"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
} from "../../../molecules/select/next-select"
|
||||
import TagInput from "../../../molecules/tag-input"
|
||||
import useOrganizeData from "./use-organize-data"
|
||||
import NestedMultiselect from "../../../categories/components/multiselect"
|
||||
import InputHeader from "../../../../components/fundamentals/input-header"
|
||||
import {
|
||||
useFeatureFlag,
|
||||
FeatureFlag,
|
||||
} from "../../../../providers/feature-flag-provider"
|
||||
|
||||
export type OrganizeFormType = {
|
||||
type: Option | null
|
||||
@@ -86,21 +86,18 @@ const OrganizeForm = ({ form }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) && (
|
||||
{isFeatureEnabled(FeatureFlag.PRODUCT_CATEGORIES) &&
|
||||
categoriesOptions?.length ? (
|
||||
<>
|
||||
<InputHeader label="Categories" className="mb-2" />
|
||||
<Controller
|
||||
name={path("categories")}
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
if (categoriesOptions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const initiallySelected = (value || []).reduce((acc, val) => {
|
||||
acc[val] = true
|
||||
return acc
|
||||
}, {})
|
||||
}, {} as Record<string, true>)
|
||||
|
||||
return (
|
||||
<NestedMultiselect
|
||||
@@ -112,7 +109,7 @@ const OrganizeForm = ({ form }: Props) => {
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<div className="mb-large" />
|
||||
|
||||
@@ -3,14 +3,13 @@ import { useMemo } from "react"
|
||||
import {
|
||||
useAdminCollections,
|
||||
useAdminProductCategories,
|
||||
useAdminProductTypes,
|
||||
useAdminProductTypes
|
||||
} from "medusa-react"
|
||||
|
||||
import { NestedMultiselectOption } from "../../../categories/components/multiselect"
|
||||
import { transformCategoryToNestedFormOptions } from "../../../categories/utils/transform-response"
|
||||
import { NestedMultiselectOption } from "../../../../domain/categories/components/multiselect"
|
||||
import { transformCategoryToNestedFormOptions } from "../../../../domain/categories/utils/transform-response"
|
||||
import {
|
||||
useFeatureFlag,
|
||||
FeatureFlag,
|
||||
FeatureFlag, useFeatureFlag
|
||||
} from "../../../../providers/feature-flag-provider"
|
||||
|
||||
const useOrganizeData = () => {
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useAdminSalesChannels } from "medusa-react"
|
||||
import React, { useContext, useMemo, useState } from "react"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { usePagination, useRowSelect, useTable } from "react-table"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../components/molecules/modal"
|
||||
import { LayeredModalContext } from "../../../../components/molecules/modal/layered-modal"
|
||||
import { useDebounce } from "../../../../hooks/use-debounce"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import { useLayeredModal } from "../../../molecules/modal/layered-modal"
|
||||
import SalesChannelTable, { useSalesChannelsTableColumns } from "./table"
|
||||
import { useSalesChannelsModal } from "./use-sales-channels-modal"
|
||||
|
||||
@@ -33,7 +33,7 @@ const AddScreen = () => {
|
||||
return salesChannels?.filter(({ id }) => !ids.includes(id)) || []
|
||||
}, [salesChannels, source])
|
||||
|
||||
const { pop, reset } = useContext(LayeredModalContext)
|
||||
const { pop, reset } = useLayeredModal()
|
||||
|
||||
const state = useTable(
|
||||
{
|
||||
@@ -120,7 +120,7 @@ const AddScreen = () => {
|
||||
}
|
||||
|
||||
export const useAddChannelsModalScreen = () => {
|
||||
const { pop } = React.useContext(LayeredModalContext)
|
||||
const { pop } = useLayeredModal()
|
||||
|
||||
return {
|
||||
title: "Add Sales Channels",
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from "react"
|
||||
import { usePagination, useRowSelect, useTable } from "react-table"
|
||||
import SalesChannelTable, {
|
||||
SalesChannelTableActions,
|
||||
useSalesChannelsTableColumns,
|
||||
} from "./table"
|
||||
import { usePagination, useRowSelect, useTable } from "react-table"
|
||||
|
||||
import Modal from "../../../../components/molecules/modal"
|
||||
import { useDebounce } from "../../../../hooks/use-debounce"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import { useSalesChannelsModal } from "./use-sales-channels-modal"
|
||||
|
||||
const LIMIT = 12
|
||||
@@ -1,10 +1,9 @@
|
||||
import { SalesChannel } from "@medusajs/medusa"
|
||||
import React from "react"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../components/molecules/modal"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../components/molecules/modal/layered-modal"
|
||||
useLayeredModal,
|
||||
} from "../../../molecules/modal/layered-modal"
|
||||
import AvailableScreen from "./available-screen"
|
||||
import { SalesChannelsModalContext } from "./use-sales-channels-modal"
|
||||
|
||||
@@ -19,7 +18,7 @@ type Props = {
|
||||
* Re-usable Sales Channels Modal, used for adding and editing sales channels both when creating a new product and editing an existing product.
|
||||
*/
|
||||
const SalesChannelsModal = ({ open, source = [], onClose, onSave }: Props) => {
|
||||
const context = React.useContext(LayeredModalContext)
|
||||
const context = useLayeredModal()
|
||||
|
||||
return (
|
||||
<SalesChannelsModalContext.Provider
|
||||
@@ -2,12 +2,12 @@ import { SalesChannel } from "@medusajs/medusa"
|
||||
import clsx from "clsx"
|
||||
import React, { useMemo } from "react"
|
||||
import { TableInstance } from "react-table"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
|
||||
import IndeterminateCheckbox from "../../../../components/molecules/indeterminate-checkbox"
|
||||
import { LayeredModalContext } from "../../../../components/molecules/modal/layered-modal"
|
||||
import Table from "../../../../components/molecules/table"
|
||||
import TableContainer from "../../../../components/organisms/table-container"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import PlusIcon from "../../../fundamentals/icons/plus-icon"
|
||||
import IndeterminateCheckbox from "../../../molecules/indeterminate-checkbox"
|
||||
import { useLayeredModal } from "../../../molecules/modal/layered-modal"
|
||||
import Table from "../../../molecules/table"
|
||||
import TableContainer from "../../../organisms/table-container"
|
||||
import { useAddChannelsModalScreen } from "./add-screen"
|
||||
|
||||
type SalesChannelsTableProps = {
|
||||
@@ -181,7 +181,7 @@ export const SalesChannelTableActions = ({
|
||||
"translate-y-[0px]": showAddChannels,
|
||||
}
|
||||
|
||||
const { push } = React.useContext(LayeredModalContext)
|
||||
const { push } = useLayeredModal()
|
||||
|
||||
return (
|
||||
<div className="space-x-xsmall flex h-[34px] overflow-hidden">
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FieldArrayWithId, useFieldArray } from "react-hook-form"
|
||||
import FileUploadField from "../../../../components/atoms/file-upload-field"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
import Actionables, {
|
||||
ActionType,
|
||||
} from "../../../../components/molecules/actionables"
|
||||
import { FormImage } from "../../../../types/shared"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import FileUploadField from "../../../atoms/file-upload-field"
|
||||
import TrashIcon from "../../../fundamentals/icons/trash-icon"
|
||||
import Actionables, {
|
||||
ActionType
|
||||
} from "../../../molecules/actionables"
|
||||
|
||||
export type ThumbnailFormType = {
|
||||
images: FormImage[]
|
||||
@@ -1,11 +1,11 @@
|
||||
import { UseFormReturn } from "react-hook-form"
|
||||
import InputError from "../../../../../components/atoms/input-error"
|
||||
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
|
||||
import Accordion from "../../../../../components/organisms/accordion"
|
||||
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 { PricesFormType } from "../../prices-form"
|
||||
import VariantGeneralForm, {
|
||||
VariantGeneralFormType,
|
||||
} from "../variant-general-form"
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useFieldArray, UseFormReturn } from "react-hook-form"
|
||||
import CustomsForm, { CustomsFormType } from "../../customs-form"
|
||||
import DimensionsForm, { DimensionsFormType } from "../../dimensions-form"
|
||||
import { UseFormReturn, useFieldArray } from "react-hook-form"
|
||||
import VariantGeneralForm, {
|
||||
VariantGeneralFormType,
|
||||
VariantGeneralFormType
|
||||
} from "../variant-general-form"
|
||||
import VariantStockForm, { VariantStockFormType } from "../variant-stock-form"
|
||||
|
||||
import Accordion from "../../../../../components/organisms/accordion"
|
||||
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import { PricesFormType } from "../../prices-form"
|
||||
import VariantPricesForm from "../variant-prices-form"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import IconTooltip from "../../../../molecules/icon-tooltip"
|
||||
import InputField from "../../../../molecules/input"
|
||||
import Accordion from "../../../../organisms/accordion"
|
||||
import { PricesFormType } from "../../../general/prices-form"
|
||||
import VariantPricesForm from "../variant-prices-form"
|
||||
|
||||
export type EditFlowVariantFormType = {
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import FormValidator from "../../../../../utils/form-validator"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import InputField from "../../../../molecules/input"
|
||||
|
||||
export type VariantGeneralFormType = {
|
||||
title: string | null
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import PricesForm, { PricesFormType } from "../../prices-form"
|
||||
import PricesForm, { PricesFormType } from "../../../general/prices-form"
|
||||
|
||||
type Props = {
|
||||
form: NestedForm<PricesFormType>
|
||||
@@ -2,8 +2,8 @@ import { isEqual } from "lodash"
|
||||
import { useMemo } from "react"
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { VariantOptionValueType } from "."
|
||||
import { AddVariantsFormType } from "../../../../../domain/products/new/add-variants"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import { AddVariantsFormType } from "../../../new/add-variants"
|
||||
|
||||
const useCheckOptions = (variantForm: NestedForm<AddVariantsFormType>) => {
|
||||
const { control: variantControl, path: variantPath } = variantForm
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
import { NextCreateableSelect } from "../../../../../components/molecules/select/next-select"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import { NextCreateableSelect } from "../../../../molecules/select/next-select"
|
||||
|
||||
export type VariantOptionValueType = {
|
||||
option_id: string
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
|
||||
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import FeatureToggle from "../../../../../components/fundamentals/feature-toggle"
|
||||
import IconBadge from "../../../../../components/fundamentals/icon-badge"
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
|
||||
import { ManageLocationsScreen } from "../../variant-inventory-form/variant-stock-form"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import React from "react"
|
||||
import Switch from "../../../../../components/atoms/switch"
|
||||
import clsx from "clsx"
|
||||
import { sum } from "lodash"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
import React from "react"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import Switch from "../../../../atoms/switch"
|
||||
import Button from "../../../../fundamentals/button"
|
||||
import FeatureToggle from "../../../../fundamentals/feature-toggle"
|
||||
import IconBadge from "../../../../fundamentals/icon-badge"
|
||||
import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon"
|
||||
import InputField from "../../../../molecules/input"
|
||||
import { LayeredModalContext } from "../../../../molecules/modal/layered-modal"
|
||||
import { ManageLocationsScreen } from "../../variant-inventory-form/variant-stock-form"
|
||||
|
||||
export type VariantStockFormType = {
|
||||
manage_inventory: boolean
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
import { InventoryLevelDTO, StockLocationDTO } from "@medusajs/medusa"
|
||||
import React, { useContext, useMemo, useState } from "react"
|
||||
|
||||
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import IconBadge from "../../../../../components/fundamentals/icon-badge"
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import { LayeredModalContext } from "../../../../../components/molecules/modal/layered-modal"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import Switch from "../../../../../components/atoms/switch"
|
||||
import { useAdminStockLocations } from "medusa-react"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import Switch from "../../../../atoms/switch"
|
||||
import Button from "../../../../fundamentals/button"
|
||||
import IconBadge from "../../../../fundamentals/icon-badge"
|
||||
import BuildingsIcon from "../../../../fundamentals/icons/buildings-icon"
|
||||
import InputField from "../../../../molecules/input"
|
||||
import Modal from "../../../../molecules/modal"
|
||||
import { useLayeredModal } from "../../../../molecules/modal/layered-modal"
|
||||
|
||||
export type VariantStockFormType = {
|
||||
manage_inventory?: boolean
|
||||
@@ -34,7 +33,7 @@ const VariantStockForm = ({ form, locationLevels }: Props) => {
|
||||
[locationLevels]
|
||||
)
|
||||
|
||||
const layeredModalContext = useContext(LayeredModalContext)
|
||||
const layeredModalContext = useLayeredModal()
|
||||
|
||||
const { stock_locations: locations, isLoading } = useAdminStockLocations()
|
||||
|
||||
@@ -238,7 +237,7 @@ const ManageLocationsForm = ({
|
||||
locationOptions,
|
||||
onSubmit,
|
||||
}: ManageLocationFormProps) => {
|
||||
const layeredModalContext = useContext(LayeredModalContext)
|
||||
const layeredModalContext = useLayeredModal()
|
||||
const { pop } = layeredModalContext
|
||||
|
||||
const existingLocations = useMemo(() => {
|
||||
@@ -2,6 +2,7 @@ import clsx from "clsx"
|
||||
import React, { useImperativeHandle } from "react"
|
||||
|
||||
import CheckIcon from "../../fundamentals/icons/check-icon"
|
||||
import MinusIcon from "../../fundamentals/icons/minus-icon"
|
||||
|
||||
type IndeterminateCheckboxProps = {
|
||||
type?: "checkbox" | "radio"
|
||||
@@ -66,6 +67,7 @@ const IndeterminateCheckbox = React.forwardRef<
|
||||
>
|
||||
<span className="self-center">
|
||||
{checked && <CheckIcon size={16} />}
|
||||
{indeterminate && <MinusIcon size={16} />}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useAdminCreateVariant } from "medusa-react"
|
||||
import React from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import FormValidator from "../../../utils/form-validator"
|
||||
import Button from "../../fundamentals/button"
|
||||
import PlusIcon from "../../fundamentals/icons/plus-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import IconTooltip from "../../molecules/icon-tooltip"
|
||||
import Modal from "../../molecules/modal"
|
||||
import CurrencyInput from "../currency-input"
|
||||
import { useValuesFieldArray } from "./use-values-field-array"
|
||||
|
||||
type AddDenominationModalProps = {
|
||||
giftCard: Omit<Product, "beforeInsert">
|
||||
storeCurrency: string
|
||||
currencyCodes: string[]
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
const AddDenominationModal: React.FC<AddDenominationModalProps> = ({
|
||||
giftCard,
|
||||
storeCurrency,
|
||||
currencyCodes,
|
||||
handleClose,
|
||||
}) => {
|
||||
const { watch, handleSubmit, control } = useForm<{
|
||||
default_price: number
|
||||
prices: {
|
||||
price: {
|
||||
amount: number
|
||||
currency_code: string
|
||||
}
|
||||
}[]
|
||||
}>()
|
||||
const notification = useNotification()
|
||||
const { mutate, isLoading } = useAdminCreateVariant(giftCard.id)
|
||||
|
||||
// passed to useValuesFieldArray so new prices are intialized with the currenct default price
|
||||
const defaultValue = watch("default_price", 10000)
|
||||
|
||||
const { fields, appendPrice, deletePrice, availableCurrencies } =
|
||||
useValuesFieldArray(
|
||||
currencyCodes,
|
||||
{
|
||||
control,
|
||||
name: "prices",
|
||||
keyName: "indexId",
|
||||
},
|
||||
{
|
||||
defaultAmount: defaultValue,
|
||||
defaultCurrencyCode: storeCurrency,
|
||||
}
|
||||
)
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const prices = [
|
||||
{
|
||||
amount: data.default_price,
|
||||
currency_code: storeCurrency,
|
||||
},
|
||||
]
|
||||
|
||||
if (data.prices) {
|
||||
data.prices.forEach((p) => {
|
||||
prices.push({
|
||||
amount: p.price.amount,
|
||||
currency_code: p.price.currency_code,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
mutate(
|
||||
{
|
||||
title: `${giftCard.variants.length}`,
|
||||
options: [
|
||||
{
|
||||
value: `${data.default_price}`,
|
||||
option_id: giftCard.options[0].id,
|
||||
},
|
||||
],
|
||||
prices,
|
||||
inventory_quantity: 0,
|
||||
manage_inventory: false,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notification("Success", "Denomination added successfully", "success")
|
||||
handleClose()
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = () => {
|
||||
// @ts-ignore
|
||||
if (error.response?.data?.type === "duplicate_error") {
|
||||
return `A denomination with that default value already exists`
|
||||
} else {
|
||||
return getErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
notification("Error", errorMessage(), "error")
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal handleClose={handleClose} isLargeModal>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Modal.Body>
|
||||
<Modal.Header handleClose={handleClose}>
|
||||
<span className="inter-xlarge-semibold">Add Denomination</span>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<div className="mb-xlarge flex-1">
|
||||
<div className="mb-base flex gap-x-2">
|
||||
<h3 className="inter-base-semibold">Default Value</h3>
|
||||
<IconTooltip content="This is the denomination in your store's default currency" />
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="default_price"
|
||||
rules={{
|
||||
required: "Default value is required",
|
||||
max: FormValidator.maxInteger("Default value", storeCurrency),
|
||||
}}
|
||||
render={({ field: { onChange, value, ref } }) => {
|
||||
return (
|
||||
<CurrencyInput.Root
|
||||
currentCurrency={storeCurrency}
|
||||
readOnly
|
||||
size="medium"
|
||||
>
|
||||
<CurrencyInput.Amount
|
||||
ref={ref}
|
||||
label="Amount"
|
||||
amount={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</CurrencyInput.Root>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-base flex gap-x-2">
|
||||
<h3 className="inter-base-semibold">Other Values</h3>
|
||||
<IconTooltip content="Here you can add values in other currencies" />
|
||||
</div>
|
||||
<div className="gap-y-xsmall flex flex-col">
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div
|
||||
key={field.indexId}
|
||||
className="mb-xsmall flex items-end last:mb-0"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Controller
|
||||
control={control}
|
||||
key={field.indexId}
|
||||
name={`prices.${index}.price`}
|
||||
rules={{
|
||||
required: FormValidator.required("Price"),
|
||||
validate: (val) => {
|
||||
return FormValidator.validateMaxInteger(
|
||||
"Price",
|
||||
val.amount,
|
||||
val.currency_code
|
||||
)
|
||||
},
|
||||
}}
|
||||
defaultValue={field.price}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const codes = [
|
||||
value?.currency_code,
|
||||
...availableCurrencies,
|
||||
]
|
||||
codes.sort()
|
||||
return (
|
||||
<CurrencyInput.Root
|
||||
currencyCodes={codes}
|
||||
currentCurrency={value?.currency_code}
|
||||
size="medium"
|
||||
readOnly={index === 0}
|
||||
onChange={(code) =>
|
||||
onChange({ ...value, currency_code: code })
|
||||
}
|
||||
>
|
||||
<CurrencyInput.Amount
|
||||
label="Amount"
|
||||
onChange={(amount) =>
|
||||
onChange({ ...value, amount })
|
||||
}
|
||||
amount={value?.amount}
|
||||
/>
|
||||
</CurrencyInput.Root>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="ml-large h-10 w-10"
|
||||
type="button"
|
||||
>
|
||||
<TrashIcon
|
||||
onClick={deletePrice(index)}
|
||||
className="text-grey-40"
|
||||
size="20"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-large mb-small">
|
||||
<Button
|
||||
onClick={appendPrice}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={availableCurrencies?.length === 0}
|
||||
>
|
||||
<PlusIcon size={20} />
|
||||
Add a price
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={handleClose}
|
||||
className="mr-2 min-w-[130px] justify-center"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="mr-2 min-w-[130px] justify-center"
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal.Body>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddDenominationModal
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useFieldArray, UseFieldArrayOptions, useWatch } from "react-hook-form"
|
||||
|
||||
type UseValuesFieldArrayOptions = {
|
||||
defaultAmount: number
|
||||
defaultCurrencyCode: string
|
||||
}
|
||||
|
||||
type ValuesFormValue = {
|
||||
price: {
|
||||
currency_code: string
|
||||
amount: number
|
||||
}
|
||||
}
|
||||
|
||||
export const useValuesFieldArray = <TKeyName extends string = "id">(
|
||||
currencyCodes: string[],
|
||||
{ control, name, keyName }: UseFieldArrayOptions<TKeyName>,
|
||||
options: UseValuesFieldArrayOptions = {
|
||||
defaultAmount: 1000,
|
||||
defaultCurrencyCode: "usd",
|
||||
}
|
||||
) => {
|
||||
const { defaultAmount } = options
|
||||
const { fields, append, remove } = useFieldArray<ValuesFormValue, TKeyName>({
|
||||
control,
|
||||
name,
|
||||
keyName,
|
||||
})
|
||||
const watchedFields = useWatch({
|
||||
control,
|
||||
name,
|
||||
defaultValue: fields,
|
||||
})
|
||||
|
||||
const selectedCurrencies = watchedFields.map(
|
||||
(field) => field?.price?.currency_code
|
||||
)
|
||||
const availableCurrencies = currencyCodes?.filter(
|
||||
(currency) => !selectedCurrencies.includes(currency)
|
||||
)
|
||||
|
||||
const controlledFields = fields.map((field, index) => {
|
||||
return {
|
||||
...field,
|
||||
...watchedFields[index],
|
||||
}
|
||||
})
|
||||
|
||||
const appendPrice = () => {
|
||||
const newCurrency = availableCurrencies[0]
|
||||
append({
|
||||
price: {
|
||||
currency_code: newCurrency,
|
||||
amount: defaultAmount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const deletePrice = (index) => {
|
||||
return () => {
|
||||
remove(index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fields: controlledFields,
|
||||
appendPrice,
|
||||
deletePrice,
|
||||
availableCurrencies,
|
||||
selectedCurrencies,
|
||||
} as const
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import _ from "lodash"
|
||||
import * as React from "react"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import Button from "../../fundamentals/button"
|
||||
import PlusIcon from "../../fundamentals/icons/plus-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import IconTooltip from "../../molecules/icon-tooltip"
|
||||
import Modal from "../../molecules/modal"
|
||||
import CurrencyInput from "../../organisms/currency-input"
|
||||
|
||||
export type PriceType = {
|
||||
currency_code: string
|
||||
amount: number
|
||||
id?: string
|
||||
}
|
||||
|
||||
type EditDenominationsModalProps = {
|
||||
defaultDenominations: PriceType[]
|
||||
handleClose: () => void
|
||||
onSubmit: (denominations: PriceType[]) => void
|
||||
defaultNewAmount?: number
|
||||
defaultNewCurrencyCode?: string
|
||||
currencyCodes?: string[]
|
||||
}
|
||||
|
||||
const EditDenominationsModal = ({
|
||||
defaultDenominations = [],
|
||||
onSubmit,
|
||||
handleClose,
|
||||
currencyCodes = [],
|
||||
defaultNewAmount = 1000,
|
||||
}: EditDenominationsModalProps) => {
|
||||
const [denominations, setDenominations] = React.useState(
|
||||
augmentWithIds(defaultDenominations)
|
||||
)
|
||||
const selectedCurrencies = denominations.map(
|
||||
(denomination) => denomination.currency_code
|
||||
)
|
||||
const availableCurrencies = currencyCodes?.filter(
|
||||
(currency) => !selectedCurrencies.includes(currency)
|
||||
)
|
||||
|
||||
const onAmountChange = (index) => {
|
||||
return (amount) => {
|
||||
const newDenominations = denominations.slice()
|
||||
newDenominations[index] = { ...newDenominations[index], amount }
|
||||
setDenominations(newDenominations)
|
||||
}
|
||||
}
|
||||
|
||||
const onCurrencyChange = (index) => {
|
||||
return (currencyCode) => {
|
||||
const newDenominations = denominations.slice()
|
||||
newDenominations[index] = {
|
||||
...newDenominations[index],
|
||||
currency_code: currencyCode,
|
||||
}
|
||||
setDenominations(newDenominations)
|
||||
}
|
||||
}
|
||||
|
||||
const onClickDelete = (index) => {
|
||||
return () => {
|
||||
const newDenominations = denominations.slice()
|
||||
newDenominations.splice(index, 1)
|
||||
setDenominations(newDenominations)
|
||||
}
|
||||
}
|
||||
|
||||
const appendDenomination = () => {
|
||||
const newDenomination = {
|
||||
amount: defaultNewAmount,
|
||||
currency_code: availableCurrencies[0],
|
||||
}
|
||||
setDenominations([...denominations, augmentWithId(newDenomination)])
|
||||
}
|
||||
|
||||
const submitHandler = () => {
|
||||
const strippedDenominations = stripDenominationFromIndexId(denominations)
|
||||
|
||||
if (onSubmit) {
|
||||
onSubmit(strippedDenominations)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal handleClose={handleClose}>
|
||||
<Modal.Body>
|
||||
<Modal.Header handleClose={handleClose}>
|
||||
<span className="inter-xlarge-semibold">Edit Denominations</span>
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
<div className="pt-1">
|
||||
<div className="flex items-center">
|
||||
<label className="inter-base-semibold text-grey-90 mr-1.5">
|
||||
Prices
|
||||
</label>
|
||||
<IconTooltip content={"Helpful denominations"} />
|
||||
</div>
|
||||
{denominations.map((field, index) => {
|
||||
return (
|
||||
<div
|
||||
key={field.indexId}
|
||||
className="mt-xsmall flex items-center first:mt-0"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<CurrencyInput.Root
|
||||
currencyCodes={currencyCodes}
|
||||
currentCurrency={field.currency_code}
|
||||
onChange={onCurrencyChange(index)}
|
||||
size="medium"
|
||||
>
|
||||
<CurrencyInput.Amount
|
||||
label="Amount"
|
||||
onChange={onAmountChange(index)}
|
||||
amount={field.amount}
|
||||
/>
|
||||
</CurrencyInput.Root>
|
||||
</div>
|
||||
<button className="ml-2xlarge">
|
||||
<TrashIcon
|
||||
onClick={onClickDelete(index)}
|
||||
className="text-grey-40"
|
||||
size="20"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-large">
|
||||
<Button
|
||||
onClick={appendDenomination}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
disabled={availableCurrencies.length === 0}
|
||||
>
|
||||
<PlusIcon size={20} />
|
||||
Add a price
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="flex w-full justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={handleClose}
|
||||
className="mr-2 min-w-[130px] justify-center"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="mr-2 min-w-[130px] justify-center"
|
||||
onClick={submitHandler}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditDenominationsModal
|
||||
|
||||
const augmentWithId = (obj) => ({ ...obj, indexId: uuidv4() })
|
||||
|
||||
const augmentWithIds = (list) => {
|
||||
return list.map(augmentWithId)
|
||||
}
|
||||
|
||||
const stripDenominationFromIndexId = (list) => {
|
||||
return list.map((element) => _.omit(element, "indexId"))
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useAdminCreateVariant, useAdminStore } from "medusa-react"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import DenominationForm, {
|
||||
DenominationFormType,
|
||||
} from "../../forms/gift-card/denomination-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* Whether the modal is open or not
|
||||
*/
|
||||
open: boolean
|
||||
/**
|
||||
* Callback to close the modal
|
||||
*/
|
||||
onClose: () => void
|
||||
/**
|
||||
* Gift card
|
||||
*/
|
||||
giftCard: Product
|
||||
}
|
||||
|
||||
type AddDenominationModalFormType = {
|
||||
denominations: DenominationFormType
|
||||
}
|
||||
|
||||
const AddDenominationModal = ({ open, onClose, giftCard }: Props) => {
|
||||
const { mutate, isLoading: isMutating } = useAdminCreateVariant(giftCard.id)
|
||||
|
||||
const { store } = useAdminStore()
|
||||
|
||||
const defaultValues: AddDenominationModalFormType | undefined =
|
||||
useMemo(() => {
|
||||
if (!store) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
denominations: {
|
||||
defaultDenomination: {
|
||||
currency_code: store.default_currency_code,
|
||||
includes_tax: store.currencies.find(
|
||||
(c) => c.code === store.default_currency_code
|
||||
)?.includes_tax,
|
||||
},
|
||||
currencyDenominations: store.currencies
|
||||
.filter((c) => c.code !== store.default_currency_code)
|
||||
.map((currency) => {
|
||||
return {
|
||||
currency_code: currency.code,
|
||||
includes_tax: currency.includes_tax,
|
||||
}
|
||||
}),
|
||||
},
|
||||
} as AddDenominationModalFormType
|
||||
}, [store])
|
||||
|
||||
const form = useForm<AddDenominationModalFormType>({
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const notification = useNotification()
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
reset(defaultValues)
|
||||
onClose()
|
||||
}, [reset, defaultValues, onClose])
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
const payload = {
|
||||
title: `${giftCard.variants.length}`,
|
||||
options: [
|
||||
{
|
||||
value: `${data.denominations.defaultDenomination.amount}`,
|
||||
option_id: giftCard.options[0].id,
|
||||
},
|
||||
],
|
||||
prices: [
|
||||
{
|
||||
amount: data.denominations.defaultDenomination.amount,
|
||||
currency_code: data.denominations.defaultDenomination.currency_code,
|
||||
},
|
||||
],
|
||||
inventory_quantity: 0,
|
||||
manage_inventory: false,
|
||||
}
|
||||
|
||||
data.denominations.currencyDenominations.forEach((currency) => {
|
||||
if (
|
||||
(currency.amount !== null && currency.amount !== undefined) ||
|
||||
data.denominations.useSameValue
|
||||
) {
|
||||
payload.prices.push({
|
||||
amount: data.denominations.useSameValue
|
||||
? data.denominations.defaultDenomination.amount
|
||||
: currency.amount,
|
||||
currency_code: currency.currency_code,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
mutate(payload, {
|
||||
onSuccess: () => {
|
||||
notification(
|
||||
"Denomination added",
|
||||
"A new denomination was succesfully added",
|
||||
"success"
|
||||
)
|
||||
handleClose()
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = () => {
|
||||
// @ts-ignore
|
||||
if (error.response?.data?.type === "duplicate_error") {
|
||||
return `A denomination with that default value already exists`
|
||||
} else {
|
||||
return getErrorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
notification("Error", errorMessage(), "error")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal open={open} handleClose={handleClose}>
|
||||
<Modal.Body>
|
||||
<Modal.Header handleClose={handleClose}>
|
||||
<h1 className="inter-xlarge-semibold">Add Denomination</h1>
|
||||
</Modal.Header>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Modal.Content>
|
||||
<DenominationForm form={nestedForm(form, "denominations")} />
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="gap-x-xsmall flex w-full items-center justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
type="submit"
|
||||
disabled={isMutating || !isDirty}
|
||||
loading={isMutating}
|
||||
>
|
||||
Save and close
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddDenominationModal
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ProductVariant } from "@medusajs/medusa"
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import Table from "../../molecules/table"
|
||||
import { useDenominationColumns } from "./use-denominations-columns"
|
||||
|
||||
type DenominationsTableProps = {
|
||||
denominations: ProductVariant[]
|
||||
}
|
||||
|
||||
const DenominationsTable = ({ denominations }: DenominationsTableProps) => {
|
||||
const columns = useDenominationColumns()
|
||||
|
||||
const { getHeaderGroups, getRowModel } = useReactTable({
|
||||
columns,
|
||||
data: denominations,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<Table.Head>
|
||||
{getHeaderGroups().map((headerGroup) => {
|
||||
return (
|
||||
<Table.HeadRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<Table.HeadCell
|
||||
key={header.id}
|
||||
className="inter-small-semibold text-grey-50"
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
maxWidth: header.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</Table.HeadCell>
|
||||
)
|
||||
})}
|
||||
</Table.HeadRow>
|
||||
)
|
||||
})}
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
{getRowModel().rows.map((row) => {
|
||||
return (
|
||||
<Table.Row key={row.id} className="last-of-type:border-b-0">
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<Table.Cell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
maxWidth: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Table.Cell>
|
||||
)
|
||||
})}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default DenominationsTable
|
||||
@@ -0,0 +1,191 @@
|
||||
import { MoneyAmount, ProductVariant, Store } from "@medusajs/medusa"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
adminProductKeys,
|
||||
useAdminStore,
|
||||
useAdminUpdateVariant,
|
||||
} from "medusa-react"
|
||||
import { useCallback, useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import DenominationForm, {
|
||||
DenominationFormType,
|
||||
} from "../../forms/gift-card/denomination-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type EditDenominationsModalProps = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
denomination: ProductVariant
|
||||
}
|
||||
|
||||
type EditDenominationModalFormType = {
|
||||
denominations: DenominationFormType
|
||||
}
|
||||
|
||||
const EditDenominationsModal = ({
|
||||
denomination,
|
||||
onClose,
|
||||
open,
|
||||
}: EditDenominationsModalProps) => {
|
||||
const { store } = useAdminStore()
|
||||
const { mutate, isLoading } = useAdminUpdateVariant(denomination.product_id)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const form = useForm<EditDenominationModalFormType>({
|
||||
defaultValues: getDefaultValues(store, denomination.prices),
|
||||
})
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
} = form
|
||||
|
||||
const notification = useNotification()
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
reset(getDefaultValues(store, denomination.prices))
|
||||
}
|
||||
}, [open, store, denomination.prices, reset])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
reset()
|
||||
onClose()
|
||||
}, [reset, onClose])
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
const payload = {
|
||||
prices: [
|
||||
{
|
||||
amount: data.denominations.defaultDenomination.amount,
|
||||
currency_code: data.denominations.defaultDenomination.currency_code,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
data.denominations.currencyDenominations.forEach((currency) => {
|
||||
if (
|
||||
(currency.amount !== undefined && currency.amount !== null) ||
|
||||
data.denominations.useSameValue
|
||||
) {
|
||||
payload.prices.push({
|
||||
amount: data.denominations.useSameValue
|
||||
? data.denominations.defaultDenomination.amount
|
||||
: currency.amount,
|
||||
currency_code: currency.currency_code,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
mutate(
|
||||
{
|
||||
variant_id: denomination.id,
|
||||
...payload,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notification(
|
||||
"Denomination updated",
|
||||
"A new denomination was succesfully updated",
|
||||
"success"
|
||||
)
|
||||
queryClient.invalidateQueries(adminProductKeys.all)
|
||||
handleClose()
|
||||
},
|
||||
onError: (error) => {
|
||||
notification("Error", getErrorMessage(error), "error")
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal open={open} handleClose={handleClose}>
|
||||
<Modal.Body>
|
||||
<Modal.Header handleClose={handleClose}>
|
||||
<h1 className="inter-xlarge-semibold">Edit Denominations</h1>
|
||||
</Modal.Header>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Modal.Content>
|
||||
<DenominationForm form={nestedForm(form, "denominations")} />
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="gap-x-2xsmall flex w-full items-center justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
type="submit"
|
||||
loading={isLoading}
|
||||
disabled={!isDirty || isLoading}
|
||||
>
|
||||
Save and close
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const getDefaultValues = (store: Store | undefined, prices: MoneyAmount[]) => {
|
||||
if (!store) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
denominations: {
|
||||
defaultDenomination: {
|
||||
currency_code: store.default_currency_code,
|
||||
includes_tax: store.currencies.find(
|
||||
(c) => c.code === store.default_currency_code
|
||||
)?.includes_tax,
|
||||
amount: findPrice(store.default_currency_code, prices),
|
||||
},
|
||||
currencyDenominations: store.currencies
|
||||
.filter((c) => c.code !== store.default_currency_code)
|
||||
.map((currency) => {
|
||||
return {
|
||||
currency_code: currency.code,
|
||||
includes_tax: currency.includes_tax,
|
||||
amount: findPrice(currency.code, prices),
|
||||
}
|
||||
}),
|
||||
},
|
||||
} as EditDenominationModalFormType
|
||||
|
||||
if (
|
||||
defaultValues.denominations.currencyDenominations.every(
|
||||
(c) => c.amount === defaultValues.denominations.defaultDenomination.amount
|
||||
)
|
||||
) {
|
||||
defaultValues.denominations.useSameValue = true
|
||||
}
|
||||
|
||||
return defaultValues
|
||||
}
|
||||
|
||||
const findPrice = (currencyCode: string, prices: MoneyAmount[]) => {
|
||||
return prices.find(
|
||||
(p) =>
|
||||
p.currency_code === currencyCode &&
|
||||
p.region_id === null &&
|
||||
p.price_list_id === null
|
||||
)?.amount
|
||||
}
|
||||
|
||||
export default EditDenominationsModal
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import PlusIcon from "../../fundamentals/icons/plus-icon"
|
||||
import Section from "../section"
|
||||
import AddDenominationModal from "./add-denominations-modal"
|
||||
import DenominationsTable from "./denominations-table"
|
||||
|
||||
type GiftCardDenominationsSectionProps = {
|
||||
giftCard: Product
|
||||
}
|
||||
|
||||
const GiftCardDenominationsSection = ({
|
||||
giftCard,
|
||||
}: GiftCardDenominationsSectionProps) => {
|
||||
const {
|
||||
state: addDenomination,
|
||||
close: closeAddDenomination,
|
||||
open: openAddDenomination,
|
||||
} = useToggleState()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
title="Denominations"
|
||||
forceDropdown
|
||||
actions={[
|
||||
{
|
||||
label: "Add Denomination",
|
||||
onClick: openAddDenomination,
|
||||
icon: <PlusIcon size={20} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="mb-large mt-base">
|
||||
<DenominationsTable denominations={[...giftCard.variants]} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<AddDenominationModal
|
||||
giftCard={giftCard}
|
||||
open={addDenomination}
|
||||
onClose={closeAddDenomination}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GiftCardDenominationsSection
|
||||
@@ -0,0 +1,176 @@
|
||||
import { MoneyAmount, ProductVariant } from "@medusajs/medusa"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useAdminDeleteVariant, useAdminStore } from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import useImperativeDialog from "../../../hooks/use-imperative-dialog"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import { normalizeAmount } from "../../../utils/prices"
|
||||
import Tooltip from "../../atoms/tooltip"
|
||||
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import Actionables, { ActionType } from "../../molecules/actionables"
|
||||
import EditDenominationsModal from "./edit-denominations-modal"
|
||||
|
||||
const columnHelper = createColumnHelper<ProductVariant>()
|
||||
|
||||
export const useDenominationColumns = () => {
|
||||
const { store } = useAdminStore()
|
||||
|
||||
const columns = useMemo(() => {
|
||||
if (!store) {
|
||||
return []
|
||||
}
|
||||
|
||||
const defaultCurrency = store.default_currency_code
|
||||
|
||||
return [
|
||||
columnHelper.display({
|
||||
header: "Denomination",
|
||||
id: "denomination",
|
||||
cell: ({ row }) => {
|
||||
const defaultDenomination = row.original.prices.find(
|
||||
(p) =>
|
||||
p.currency_code === defaultCurrency &&
|
||||
p.region_id === null &&
|
||||
p.price_list_id === null
|
||||
)
|
||||
|
||||
return defaultDenomination ? (
|
||||
<p>
|
||||
{normalizeAmount(defaultCurrency, defaultDenomination.amount)}{" "}
|
||||
<span className="text-grey-50">
|
||||
{defaultCurrency.toUpperCase()}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
header: "In other currencies",
|
||||
id: "other_currencies",
|
||||
cell: ({ row }) => {
|
||||
const otherCurrencies = row.original.prices.filter(
|
||||
(p) =>
|
||||
p.currency_code !== defaultCurrency &&
|
||||
p.region_id === null &&
|
||||
p.price_list_id === null
|
||||
)
|
||||
|
||||
let remainder: MoneyAmount[] = []
|
||||
|
||||
if (otherCurrencies.length > 2) {
|
||||
remainder = otherCurrencies.splice(2)
|
||||
}
|
||||
|
||||
return otherCurrencies.length > 0 ? (
|
||||
<p>
|
||||
{otherCurrencies.map((p, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{normalizeAmount(p.currency_code, p.amount)}{" "}
|
||||
<span className="text-grey-50">
|
||||
{p.currency_code.toUpperCase()}
|
||||
</span>
|
||||
{index < otherCurrencies.length - 1 && ", "}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{remainder.length > 0 && (
|
||||
<Tooltip
|
||||
content={
|
||||
<ul>
|
||||
{remainder.map((p, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
<p>
|
||||
{normalizeAmount(p.currency_code, p.amount)}{" "}
|
||||
<span className="text-grey-50">
|
||||
{p.currency_code.toUpperCase()}
|
||||
</span>
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
>
|
||||
<span className="text-grey-50 cursor-default">{`, and ${remainder.length} more`}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
cell: ({ row: { original } }) => <Actions original={original} />,
|
||||
}),
|
||||
]
|
||||
}, [store])
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
const Actions = ({ original }: { original: ProductVariant }) => {
|
||||
const { state, open, close } = useToggleState()
|
||||
|
||||
const { mutateAsync } = useAdminDeleteVariant(original.product_id)
|
||||
const notification = useNotification()
|
||||
|
||||
const dialog = useImperativeDialog()
|
||||
|
||||
const onDelete = async () => {
|
||||
const shouldDelete = await dialog({
|
||||
heading: "Delete denomination",
|
||||
text: "Are you sure you want to delete this denomination?",
|
||||
})
|
||||
|
||||
if (shouldDelete) {
|
||||
mutateAsync(original.id, {
|
||||
onSuccess: () => {
|
||||
notification(
|
||||
"Denomination deleted",
|
||||
"Denomination was successfully deleted",
|
||||
"success"
|
||||
)
|
||||
},
|
||||
onError: (error) => {
|
||||
notification("Error", getErrorMessage(error), "error")
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const actions: ActionType[] = [
|
||||
{
|
||||
label: "Edit",
|
||||
onClick: open,
|
||||
icon: <EditIcon size={20} />,
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
onClick: onDelete,
|
||||
icon: <TrashIcon size={20} />,
|
||||
variant: "danger",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<Actionables actions={actions} forceDropdown />
|
||||
</div>
|
||||
<EditDenominationsModal
|
||||
open={state}
|
||||
onClose={close}
|
||||
denomination={original}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import { countries } from "../../../../../utils/countries"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import CustomsForm, { CustomsFormType } from "../../../components/customs-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import { countries } from "../../../utils/countries"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import CustomsForm, { CustomsFormType } from "../../forms/product/customs-form"
|
||||
import DimensionsForm, {
|
||||
DimensionsFormType,
|
||||
} from "../../../components/dimensions-form"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
} from "../../forms/product/dimensions-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
|
||||
import { ActionType } from "../../../../../components/molecules/actionables"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||
import { ActionType } from "../../molecules/actionables"
|
||||
import Section from "../section"
|
||||
import AttributeModal from "./attribute-modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const AttributesSection = ({ product }: Props) => {
|
||||
const ProductAttributesSection = ({ product }: Props) => {
|
||||
const { state, toggle, close } = useToggleState()
|
||||
|
||||
const actions: ActionType[] = [
|
||||
@@ -64,4 +64,4 @@ const Attribute = ({ attribute, value }: AttributeProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default AttributesSection
|
||||
export default ProductAttributesSection
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||
import { useAdminUpdateProduct } from "medusa-react"
|
||||
import SalesChannelsModal from "../../../components/sales-channels-modal"
|
||||
import SalesChannelsModal from "../../forms/product/sales-channels-modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import DiscountableForm, {
|
||||
DiscountableFormType,
|
||||
} from "../../../components/discountable-form"
|
||||
import GeneralForm, { GeneralFormType } from "../../../components/general-form"
|
||||
} from "../../forms/product/discountable-form"
|
||||
import GeneralForm, { GeneralFormType } from "../../forms/product/general-form"
|
||||
import OrganizeForm, {
|
||||
OrganizeFormType,
|
||||
} from "../../../components/organize-form"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
} from "../../forms/product/organize-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -39,7 +39,7 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
reset(getDefaultValues(product))
|
||||
}, [product])
|
||||
}, [product, reset])
|
||||
|
||||
const onReset = () => {
|
||||
reset(getDefaultValues(product))
|
||||
@@ -90,12 +90,20 @@ const GeneralModal = ({ product, open, onClose }: Props) => {
|
||||
</Modal.Header>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Modal.Content>
|
||||
<GeneralForm form={nestedForm(form, "general")} />
|
||||
<GeneralForm
|
||||
form={nestedForm(form, "general")}
|
||||
isGiftCard={product.is_giftcard}
|
||||
/>
|
||||
<div className="my-xlarge">
|
||||
<h2 className="inter-base-semibold mb-base">Organize Product</h2>
|
||||
<h2 className="inter-base-semibold mb-base">
|
||||
Organize {product.is_giftcard ? "Gift Card" : "Product"}
|
||||
</h2>
|
||||
<OrganizeForm form={nestedForm(form, "organize")} />
|
||||
</div>
|
||||
<DiscountableForm form={nestedForm(form, "discountable")} />
|
||||
<DiscountableForm
|
||||
form={nestedForm(form, "discountable")}
|
||||
isGiftCard={product.is_giftcard}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<div className="flex w-full justify-end gap-x-2">
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import FeatureToggle from "../../../../../components/fundamentals/feature-toggle"
|
||||
import ChannelsIcon from "../../../../../components/fundamentals/icons/channels-icon"
|
||||
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
|
||||
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
|
||||
import { ActionType } from "../../../../../components/molecules/actionables"
|
||||
import SalesChannelsDisplay from "../../../../../components/molecules/sales-channels-display"
|
||||
import StatusSelector from "../../../../../components/molecules/status-selector"
|
||||
import DelimitedList from "../../../../../components/molecules/delimited-list"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import {
|
||||
useFeatureFlag,
|
||||
FeatureFlag,
|
||||
} from "../../../../../providers/feature-flag-provider"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
useFeatureFlag,
|
||||
} from "../../../providers/feature-flag-provider"
|
||||
import FeatureToggle from "../../fundamentals/feature-toggle"
|
||||
import ChannelsIcon from "../../fundamentals/icons/channels-icon"
|
||||
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import { ActionType } from "../../molecules/actionables"
|
||||
import DelimitedList from "../../molecules/delimited-list"
|
||||
import SalesChannelsDisplay from "../../molecules/sales-channels-display"
|
||||
import StatusSelector from "../../molecules/status-selector"
|
||||
import Section from "../section"
|
||||
import ChannelsModal from "./channels-modal"
|
||||
import GeneralModal from "./general-modal"
|
||||
|
||||
@@ -21,7 +21,7 @@ type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const GeneralSection = ({ product }: Props) => {
|
||||
const ProductGeneralSection = ({ product }: Props) => {
|
||||
const { onDelete, onStatusChange } = useEditProductActions(product.id)
|
||||
const {
|
||||
state: infoState,
|
||||
@@ -174,4 +174,4 @@ const ProductSalesChannels = ({ product }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralSection
|
||||
export default ProductGeneralSection
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { ActionType } from "../../../../../components/molecules/actionables"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import { ActionType } from "../../molecules/actionables"
|
||||
import Section from "../../organisms/section"
|
||||
import MediaModal from "./media-modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const MediaSection = ({ product }: Props) => {
|
||||
const ProductMediaSection = ({ product }: Props) => {
|
||||
const { state, close, toggle } = useToggleState()
|
||||
|
||||
const actions: ActionType[] = [
|
||||
@@ -46,4 +46,4 @@ const MediaSection = ({ product }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaSection
|
||||
export default ProductMediaSection
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import useNotification from "../../../../../hooks/use-notification"
|
||||
import { FormImage } from "../../../../../types/shared"
|
||||
import { prepareImages } from "../../../../../utils/images"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import MediaForm, { MediaFormType } from "../../../components/media-form"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import { FormImage } from "../../../types/shared"
|
||||
import { prepareImages } from "../../../utils/images"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import MediaForm, { MediaFormType } from "../../forms/product/media-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -36,7 +36,7 @@ const MediaModal = ({ product, open, onClose }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
reset(getDefaultValues(product))
|
||||
}, [product])
|
||||
}, [product, reset])
|
||||
|
||||
const onReset = () => {
|
||||
reset(getDefaultValues(product))
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import JSONView from "../../../../../components/molecules/json-view"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import JSONView from "../../molecules/json-view"
|
||||
import Section from "../section"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
/** Temporary component, should be replaced with <RawJson /> but since the design is different we will use this to not break the existing design across admin. */
|
||||
const RawSection = ({ product }: Props) => {
|
||||
const ProductRawSection = ({ product }: Props) => {
|
||||
return (
|
||||
<Section title="Raw Product">
|
||||
<Section title={product.is_giftcard ? "Raw Gift Card" : "Raw Product"}>
|
||||
<div className="pt-base">
|
||||
<JSONView data={product} />
|
||||
</div>
|
||||
@@ -17,4 +17,4 @@ const RawSection = ({ product }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default RawSection
|
||||
export default ProductRawSection
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import clsx from "clsx"
|
||||
import TwoStepDelete from "../../../../../components/atoms/two-step-delete"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import useNotification from "../../../../../hooks/use-notification"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import { getErrorMessage } from "../../../../../utils/error-messages"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import TwoStepDelete from "../../atoms/two-step-delete"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Section from "../../organisms/section"
|
||||
import ThumbnailModal from "./thumbnail-modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const TumbnailSection = ({ product }: Props) => {
|
||||
const ProductThumbnailSection = ({ product }: Props) => {
|
||||
const { onUpdate, updating } = useEditProductActions(product.id)
|
||||
const { state, toggle, close } = useToggleState()
|
||||
|
||||
@@ -78,4 +78,4 @@ const TumbnailSection = ({ product }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default TumbnailSection
|
||||
export default ProductThumbnailSection
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import useNotification from "../../../../../hooks/use-notification"
|
||||
import { FormImage } from "../../../../../types/shared"
|
||||
import { prepareImages } from "../../../../../utils/images"
|
||||
import { nestedForm } from "../../../../../utils/nested-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import { FormImage } from "../../../types/shared"
|
||||
import { prepareImages } from "../../../utils/images"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import ThumbnailForm, {
|
||||
ThumbnailFormType,
|
||||
} from "../../../components/thumbnail-form"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
} from "../../forms/product/thumbnail-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -38,7 +38,7 @@ const ThumbnailModal = ({ product, open, onClose }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
reset(getDefaultValues(product))
|
||||
}, [product])
|
||||
}, [product, reset])
|
||||
|
||||
const onReset = () => {
|
||||
reset(getDefaultValues(product))
|
||||
@@ -1,17 +1,17 @@
|
||||
import { AdminPostProductsProductVariantsReq, Product } from "@medusajs/medusa"
|
||||
import { useContext, useEffect } from "react"
|
||||
import EditFlowVariantForm, {
|
||||
EditFlowVariantFormType,
|
||||
} from "../../../components/variant-form/edit-flow-variant-form"
|
||||
} from "../../forms/product/variant-form/edit-flow-variant-form"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../../components/molecules/modal/layered-modal"
|
||||
import { useContext, useEffect } from "react"
|
||||
} from "../../molecules/modal/layered-modal"
|
||||
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useMedusa } from "medusa-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@@ -32,7 +32,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
reset(getDefaultValues(product))
|
||||
}, [product])
|
||||
}, [product, reset])
|
||||
|
||||
const resetAndClose = () => {
|
||||
reset(getDefaultValues(product))
|
||||
@@ -40,7 +40,7 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
|
||||
}
|
||||
|
||||
const createStockLocationsForVariant = async (
|
||||
productRes,
|
||||
productRes: Product,
|
||||
stock_locations: { stocked_quantity: number; location_id: string }[]
|
||||
) => {
|
||||
const { variants } = productRes
|
||||
@@ -48,9 +48,11 @@ const AddVariantModal = ({ open, onClose, product }: Props) => {
|
||||
const pvMap = new Map(product.variants.map((v) => [v.id, true]))
|
||||
const addedVariant = variants.find((variant) => !pvMap.get(variant.id))
|
||||
|
||||
const inventory = await client.admin.variants.getInventory(addedVariant.id)
|
||||
if (!addedVariant) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log(inventory)
|
||||
const inventory = await client.admin.variants.getInventory(addedVariant.id)
|
||||
|
||||
await Promise.all(
|
||||
inventory.variant.inventory
|
||||
@@ -4,21 +4,20 @@ import {
|
||||
ProductVariant,
|
||||
VariantInventory,
|
||||
} from "@medusajs/medusa"
|
||||
import { useMedusa } from "medusa-react"
|
||||
import { useAdminVariantsInventory } from "medusa-react"
|
||||
import { useAdminVariantsInventory, useMedusa } from "medusa-react"
|
||||
import { useContext } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import { removeNullish } from "../../../utils/remove-nullish"
|
||||
import EditFlowVariantForm, {
|
||||
EditFlowVariantFormType,
|
||||
} from "../../../components/variant-inventory-form/edit-flow-variant-form"
|
||||
} from "../../forms/product/variant-form/edit-flow-variant-form"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../../components/molecules/modal/layered-modal"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
} from "../../molecules/modal/layered-modal"
|
||||
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import { removeNullish } from "../../../../../utils/remove-nullish"
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
import EditFlowVariantForm, {
|
||||
EditFlowVariantFormType,
|
||||
} from "../../../components/variant-form/edit-flow-variant-form"
|
||||
} from "../../forms/product/variant-form/edit-flow-variant-form"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../../components/molecules/modal/layered-modal"
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
} from "../../molecules/modal/layered-modal"
|
||||
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import { countries } from "../../../../../utils/countries"
|
||||
import { useMedusa } from "medusa-react"
|
||||
import { useContext } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import { countries } from "../../../utils/countries"
|
||||
import Button from "../../fundamentals/button"
|
||||
import Modal from "../../molecules/modal"
|
||||
import { createAddPayload } from "./add-variant-modal"
|
||||
import { createUpdatePayload } from "./edit-variants-modal/edit-variant-screen"
|
||||
import { useContext } from "react"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useMedusa } from "medusa-react"
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@@ -51,7 +51,7 @@ const EditVariantModal = ({
|
||||
const { client } = useMedusa()
|
||||
|
||||
const createStockLocationsForVariant = async (
|
||||
productRes,
|
||||
productRes: Product,
|
||||
stock_locations: { stocked_quantity: number; location_id: string }[]
|
||||
) => {
|
||||
const { variants } = productRes
|
||||
@@ -59,6 +59,10 @@ const EditVariantModal = ({
|
||||
const pvMap = new Map(product.variants.map((v) => [v.id, true]))
|
||||
const addedVariant = variants.find((variant) => !pvMap.get(variant.id))
|
||||
|
||||
if (!addedVariant) {
|
||||
return
|
||||
}
|
||||
|
||||
const inventory = await client.admin.variants.getInventory(addedVariant.id)
|
||||
|
||||
await Promise.all(
|
||||
@@ -3,18 +3,18 @@ import {
|
||||
Product,
|
||||
ProductVariant,
|
||||
} from "@medusajs/medusa"
|
||||
import React, { useContext, useEffect, useMemo } from "react"
|
||||
import EditFlowVariantForm, {
|
||||
EditFlowVariantFormType,
|
||||
} from "../../../../components/variant-form/edit-flow-variant-form"
|
||||
import React, { useContext, useEffect, useMemo } from "react"
|
||||
} from "../../../forms/product/variant-form/edit-flow-variant-form"
|
||||
|
||||
import Button from "../../../../../../components/fundamentals/button"
|
||||
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
|
||||
import Modal from "../../../../../../components/molecules/modal"
|
||||
import { getEditVariantDefaultValues } from "../edit-variant-modal"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import { useEditVariantsModal } from "./use-edit-variants-modal"
|
||||
import { useForm } from "react-hook-form"
|
||||
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import { LayeredModalContext } from "../../../molecules/modal/layered-modal"
|
||||
import { getEditVariantDefaultValues } from "../edit-variant-modal"
|
||||
import { useEditVariantsModal } from "./use-edit-variants-modal"
|
||||
|
||||
type Props = {
|
||||
variant: ProductVariant
|
||||
@@ -27,23 +27,25 @@ const EditVariantScreen = ({ variant, product }: Props) => {
|
||||
defaultValues: getEditVariantDefaultValues(variant, product),
|
||||
})
|
||||
|
||||
const { reset: formReset } = form
|
||||
|
||||
const { pop, reset } = useContext(LayeredModalContext)
|
||||
const { updatingVariant, onUpdateVariant } = useEditProductActions(product.id)
|
||||
|
||||
const popAndReset = () => {
|
||||
form.reset(getEditVariantDefaultValues(variant, product))
|
||||
formReset(getEditVariantDefaultValues(variant, product))
|
||||
pop()
|
||||
}
|
||||
|
||||
const closeAndReset = () => {
|
||||
form.reset(getEditVariantDefaultValues(variant, product))
|
||||
formReset(getEditVariantDefaultValues(variant, product))
|
||||
reset()
|
||||
onClose()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
form.reset(getEditVariantDefaultValues(variant, product))
|
||||
}, [variant, product])
|
||||
formReset(getEditVariantDefaultValues(variant, product))
|
||||
}, [variant, product, formReset])
|
||||
|
||||
const onSubmitAndBack = form.handleSubmit((data) => {
|
||||
// @ts-ignore
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
useFieldArray,
|
||||
useForm,
|
||||
} from "react-hook-form"
|
||||
import Button from "../../../../../../components/fundamentals/button"
|
||||
import Modal from "../../../../../../components/molecules/modal"
|
||||
import useEditProductActions from "../../../../hooks/use-edit-product-actions"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import Modal from "../../../molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
} from "../../../../../../components/molecules/modal/layered-modal"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
} from "../../../molecules/modal/layered-modal"
|
||||
import { EditVariantsModalContext } from "./use-edit-variants-modal"
|
||||
import { VariantCard } from "./variant-card"
|
||||
|
||||
@@ -56,6 +56,7 @@ const EditVariantsModal = ({ open, onClose, product }: Props) => {
|
||||
|
||||
const moveCard = useCallback((dragIndex: number, hoverIndex: number) => {
|
||||
move(dragIndex, hoverIndex)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const renderCard = useCallback(
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useContext } from "react"
|
||||
|
||||
type EditVariantsModalContext = {
|
||||
type EditVariantsModalContextType = {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const EditVariantsModalContext =
|
||||
React.createContext<EditVariantsModalContext | null>(null)
|
||||
React.createContext<EditVariantsModalContextType | null>(null)
|
||||
|
||||
export const useEditVariantsModal = () => {
|
||||
const context = useContext(EditVariantsModalContext)
|
||||
@@ -5,17 +5,15 @@ import { useContext, useMemo, useRef } from "react"
|
||||
import { useDrag, useDrop } from "react-dnd"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { VariantItem } from "."
|
||||
import Button from "../../../../../../components/fundamentals/button"
|
||||
import EditIcon from "../../../../../../components/fundamentals/icons/edit-icon"
|
||||
import GripIcon from "../../../../../../components/fundamentals/icons/grip-icon"
|
||||
import MoreHorizontalIcon from "../../../../../../components/fundamentals/icons/more-horizontal-icon"
|
||||
import Actionables, {
|
||||
ActionType,
|
||||
} from "../../../../../../components/molecules/actionables"
|
||||
import InputField from "../../../../../../components/molecules/input"
|
||||
import { LayeredModalContext } from "../../../../../../components/molecules/modal/layered-modal"
|
||||
import { DragItem } from "../../../../../../types/shared"
|
||||
import FormValidator from "../../../../../../utils/form-validator"
|
||||
import { DragItem } from "../../../../types/shared"
|
||||
import FormValidator from "../../../../utils/form-validator"
|
||||
import Button from "../../../fundamentals/button"
|
||||
import EditIcon from "../../../fundamentals/icons/edit-icon"
|
||||
import GripIcon from "../../../fundamentals/icons/grip-icon"
|
||||
import MoreHorizontalIcon from "../../../fundamentals/icons/more-horizontal-icon"
|
||||
import Actionables, { ActionType } from "../../../molecules/actionables"
|
||||
import InputField from "../../../molecules/input"
|
||||
import { LayeredModalContext } from "../../../molecules/modal/layered-modal"
|
||||
import { useEditVariantScreen } from "./edit-variant-screen"
|
||||
|
||||
const ItemTypes = {
|
||||
@@ -1,29 +1,26 @@
|
||||
import OptionsProvider, { useOptionsContext } from "./options-provider"
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
import OptionsProvider, { useOptionsContext } from "./options-provider"
|
||||
|
||||
import { ActionType } from "../../../../../components/molecules/actionables"
|
||||
import { useState } from "react"
|
||||
import useEditProductActions from "../../../hooks/use-edit-product-actions"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||
import GearIcon from "../../fundamentals/icons/gear-icon"
|
||||
import PlusIcon from "../../fundamentals/icons/plus-icon"
|
||||
import { ActionType } from "../../molecules/actionables"
|
||||
import Section from "../../organisms/section"
|
||||
import AddVariantModal from "./add-variant-modal"
|
||||
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
|
||||
import EditVariantInventoryModal from "./edit-variant-inventory-modal"
|
||||
import EditVariantModal from "./edit-variant-modal"
|
||||
import EditVariantsModal from "./edit-variants-modal"
|
||||
import GearIcon from "../../../../../components/fundamentals/icons/gear-icon"
|
||||
import OptionsModal from "./options-modal"
|
||||
import PlusIcon from "../../../../../components/fundamentals/icons/plus-icon"
|
||||
import Section from "../../../../../components/organisms/section"
|
||||
import VariantsTable from "./table"
|
||||
import useEditProductActions from "../../hooks/use-edit-product-actions"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { useState } from "react"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const VariantsSection = ({ product }: Props) => {
|
||||
const { isFeatureEnabled } = useFeatureFlag()
|
||||
|
||||
const ProductVariantsSection = ({ product }: Props) => {
|
||||
const [variantToEdit, setVariantToEdit] = useState<
|
||||
| {
|
||||
base: ProductVariant
|
||||
@@ -202,4 +199,4 @@ const ProductOptions = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default VariantsSection
|
||||
export default ProductVariantsSection
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
} from "medusa-react"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import PlusIcon from "../../../../../components/fundamentals/icons/plus-icon"
|
||||
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import useNotification from "../../../../../hooks/use-notification"
|
||||
import FormValidator from "../../../../../utils/form-validator"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import FormValidator from "../../../utils/form-validator"
|
||||
import Button from "../../fundamentals/button"
|
||||
import PlusIcon from "../../fundamentals/icons/plus-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import InputField from "../../molecules/input"
|
||||
import Modal from "../../molecules/modal"
|
||||
import { useOptionsContext } from "./options-provider"
|
||||
|
||||
type Props = {
|
||||
@@ -8,7 +8,7 @@ type OptionsContext = {
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
const OptionsContext = createContext<OptionsContext | null>(null)
|
||||
const OptionsContextType = createContext<OptionsContext | null>(null)
|
||||
|
||||
type Props = {
|
||||
product: Product
|
||||
@@ -30,14 +30,14 @@ const OptionsProvider = ({ product, children }: Props) => {
|
||||
}, [products, status])
|
||||
|
||||
return (
|
||||
<OptionsContext.Provider value={{ options, status, refetch }}>
|
||||
<OptionsContextType.Provider value={{ options, status, refetch }}>
|
||||
{children}
|
||||
</OptionsContext.Provider>
|
||||
</OptionsContextType.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useOptionsContext = () => {
|
||||
const context = useContext(OptionsContext)
|
||||
const context = useContext(OptionsContextType)
|
||||
if (!context) {
|
||||
throw new Error("useOptionsContext must be used within a OptionsProvider")
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Column, useTable } from "react-table"
|
||||
|
||||
import Actionables from "../../../../../components/molecules/actionables"
|
||||
import BuildingsIcon from "../../../../../components/fundamentals/icons/buildings-icon"
|
||||
import DuplicateIcon from "../../../../../components/fundamentals/icons/duplicate-icon"
|
||||
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
|
||||
import { ProductVariant } from "@medusajs/medusa"
|
||||
import Table from "../../../../../components/molecules/table"
|
||||
import TrashIcon from "../../../../../components/fundamentals/icons/trash-icon"
|
||||
import { useFeatureFlag } from "../../../../../providers/feature-flag-provider"
|
||||
import { useMemo } from "react"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
import BuildingsIcon from "../../fundamentals/icons/buildings-icon"
|
||||
import DuplicateIcon from "../../fundamentals/icons/duplicate-icon"
|
||||
import EditIcon from "../../fundamentals/icons/edit-icon"
|
||||
import TrashIcon from "../../fundamentals/icons/trash-icon"
|
||||
import Actionables from "../../molecules/actionables"
|
||||
import Table from "../../molecules/table"
|
||||
|
||||
type Props = {
|
||||
variants: ProductVariant[]
|
||||
@@ -1,119 +0,0 @@
|
||||
import { useAdminDeleteVariant } from "medusa-react"
|
||||
import React, { useState } from "react"
|
||||
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
|
||||
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
|
||||
import Table from "../../../components/molecules/table"
|
||||
import DeletePrompt from "../../../components/organisms/delete-prompt"
|
||||
import { stringDisplayPrice } from "../../../utils/prices"
|
||||
|
||||
type DenominationTableProps = {
|
||||
giftCardId: string
|
||||
denominations: any[]
|
||||
defaultCurrency: string
|
||||
setEditDenom: (denom) => void
|
||||
}
|
||||
|
||||
const DenominationTable: React.FC<DenominationTableProps> = ({
|
||||
giftCardId,
|
||||
denominations,
|
||||
defaultCurrency,
|
||||
setEditDenom,
|
||||
}) => {
|
||||
const [selectedDenom, setSelectedDenom] = useState<string | null>(null)
|
||||
|
||||
const deleteDenomination = useAdminDeleteVariant(giftCardId)
|
||||
|
||||
const getDenominationPrices = (denomination) => {
|
||||
const sortHelper = (p1, p2) => {
|
||||
const curr1 = p1.currency_code
|
||||
const curr2 = p2.currency_code
|
||||
|
||||
if (curr1 < curr2) {
|
||||
return -1
|
||||
}
|
||||
if (curr1 > curr2) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return denomination.prices
|
||||
.filter((p) => p.currency_code !== defaultCurrency) // without default
|
||||
.sort(sortHelper) // sort by currency code
|
||||
.map((p) =>
|
||||
stringDisplayPrice({ currencyCode: p.currency_code, amount: p.amount })
|
||||
) // get formatted price
|
||||
.join(", ") // concatenate to single comma separated string
|
||||
}
|
||||
|
||||
const getDenominationRow = (denomination, index) => {
|
||||
let defaultPrice = denomination.prices.find(
|
||||
(p) => p.currency_code === defaultCurrency
|
||||
)
|
||||
|
||||
if (!defaultPrice) {
|
||||
defaultPrice = denomination.prices[0]
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.Row
|
||||
key={`denomination-${index}`}
|
||||
color={"inherit"}
|
||||
actions={[
|
||||
{
|
||||
label: "Edit denomination",
|
||||
onClick: () => setEditDenom(denomination),
|
||||
icon: <EditIcon size={20} />,
|
||||
},
|
||||
{
|
||||
label: "Delete denomination",
|
||||
variant: "danger",
|
||||
onClick: () => setSelectedDenom(denomination.id),
|
||||
icon: <TrashIcon size={20} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Table.Cell>
|
||||
{stringDisplayPrice({
|
||||
currencyCode: defaultPrice.currency_code,
|
||||
amount: defaultPrice.amount,
|
||||
})}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{getDenominationPrices(denomination)}</Table.Cell>
|
||||
<Table.Cell></Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
}
|
||||
|
||||
const handleDeleteDenomination = async () => {
|
||||
deleteDenomination.mutateAsync(selectedDenom!)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
<Table>
|
||||
<Table.Head>
|
||||
<Table.HeadRow>
|
||||
<Table.HeadCell>Default Value</Table.HeadCell>
|
||||
<Table.HeadCell>Other Values</Table.HeadCell>
|
||||
</Table.HeadRow>
|
||||
</Table.Head>
|
||||
<Table.Body className="text-grey-90">
|
||||
{denominations?.map((d, idx) => getDenominationRow(d, idx))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
{selectedDenom && (
|
||||
<DeletePrompt
|
||||
handleClose={() => setSelectedDenom(null)}
|
||||
text="Are you sure you want to delete this denomination from your Medusa Store?"
|
||||
heading="Delete denomination"
|
||||
onDelete={() => handleDeleteDenomination()}
|
||||
successText="Successfully deleted denomination"
|
||||
confirmText="Yes, delete"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DenominationTable
|
||||
@@ -1,92 +0,0 @@
|
||||
import { useAdminUpdateProduct } from "medusa-react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { FormProvider, useForm, useFormContext } from "react-hook-form"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
import { handleFormError } from "../../../../utils/handle-form-error"
|
||||
import { trimValues } from "../../../../utils/trim-values"
|
||||
import { ManageGiftCardFormData } from "../utils/types"
|
||||
import { formValuesToUpdateGiftCardMapper } from "./mappers"
|
||||
|
||||
type GiftCardFormProviderProps = {
|
||||
giftCardId: string
|
||||
giftCard: ManageGiftCardFormData
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const GiftCardFormProvider = ({
|
||||
giftCardId,
|
||||
giftCard,
|
||||
children,
|
||||
}: GiftCardFormProviderProps) => {
|
||||
const [imageDirtyState, setImageDirtyState] = useState(false)
|
||||
|
||||
const methods = useForm<ManageGiftCardFormData>()
|
||||
|
||||
const resetForm = () => {
|
||||
methods.reset({
|
||||
handle: giftCard.handle || undefined,
|
||||
description: giftCard.description || undefined,
|
||||
subtitle: giftCard.subtitle || undefined,
|
||||
tags: giftCard.tags || [],
|
||||
title: giftCard.title || undefined,
|
||||
type: giftCard.type || undefined,
|
||||
thumbnail: giftCard.thumbnail || 0,
|
||||
images: giftCard.images || [],
|
||||
})
|
||||
setImageDirtyState(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resetForm()
|
||||
}, [giftCard])
|
||||
|
||||
const { mutate: update } = useAdminUpdateProduct(giftCardId)
|
||||
const notification = useNotification()
|
||||
|
||||
const onUpdate = methods.handleSubmit(async (values) => {
|
||||
const cleanedValues = trimValues(values)
|
||||
|
||||
const payload = await formValuesToUpdateGiftCardMapper(cleanedValues)
|
||||
|
||||
update(payload, {
|
||||
onSuccess: () => {
|
||||
notification("Success", "Product updated successfully", "success")
|
||||
},
|
||||
})
|
||||
}, handleFormError)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<GiftCardFormContext.Provider
|
||||
value={{
|
||||
onUpdate,
|
||||
resetForm,
|
||||
setImageDirtyState,
|
||||
additionalDirtyState: {
|
||||
images: imageDirtyState,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<form>{children}</form>
|
||||
</GiftCardFormContext.Provider>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const GiftCardFormContext = React.createContext<{
|
||||
onUpdate: (
|
||||
e?: React.BaseSyntheticEvent<object, any, any> | undefined
|
||||
) => Promise<void>
|
||||
resetForm: () => void
|
||||
setImageDirtyState: (value: boolean) => void
|
||||
additionalDirtyState: Record<string, boolean>
|
||||
} | null>(null)
|
||||
|
||||
export const useGiftCardForm = () => {
|
||||
const context = React.useContext(GiftCardFormContext)
|
||||
const form = useFormContext<ManageGiftCardFormData>()
|
||||
if (!context) {
|
||||
throw new Error("useGiftCardForm must be a child of GiftCardFormContext")
|
||||
}
|
||||
return { form, ...context }
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { AdminPostProductsProductReq, Product } from "@medusajs/medusa"
|
||||
import { prepareImages } from "../../../../utils/images"
|
||||
import { ManageGiftCardFormData } from "../utils/types"
|
||||
|
||||
export const formValuesToUpdateGiftCardMapper = async (
|
||||
values: ManageGiftCardFormData
|
||||
): Promise<AdminPostProductsProductReq> => {
|
||||
const imagePayload = {} as Pick<
|
||||
AdminPostProductsProductReq,
|
||||
"images" | "thumbnail"
|
||||
>
|
||||
|
||||
if (values.images?.length) {
|
||||
const images = await prepareImages(values.images)
|
||||
imagePayload.images = images.map((img) => img.url)
|
||||
}
|
||||
|
||||
if (values.thumbnail) {
|
||||
imagePayload.thumbnail = imagePayload.images?.length
|
||||
? imagePayload.images[values.thumbnail]
|
||||
: undefined
|
||||
}
|
||||
|
||||
return {
|
||||
title: values.title,
|
||||
subtitle: values.subtitle ?? undefined,
|
||||
description: values.description ?? undefined,
|
||||
handle: values.handle ?? undefined,
|
||||
type: values.type
|
||||
? { id: values.type?.value, value: values.type.label }
|
||||
: undefined,
|
||||
tags: values.tags?.map((tag) => ({ value: tag })) ?? [],
|
||||
// @ts-ignore
|
||||
sales_channels: undefined,
|
||||
...imagePayload,
|
||||
}
|
||||
}
|
||||
|
||||
export const giftCardToFormValuesMapper = (
|
||||
giftCard: Product
|
||||
): ManageGiftCardFormData => {
|
||||
let thumbnail = giftCard.images?.length
|
||||
? giftCard.images.findIndex((img) => img.url === giftCard.thumbnail)
|
||||
: 0
|
||||
thumbnail = thumbnail === -1 ? 0 : thumbnail
|
||||
|
||||
return {
|
||||
title: giftCard.title,
|
||||
subtitle: giftCard.subtitle,
|
||||
description: giftCard.description,
|
||||
handle: giftCard.handle || undefined,
|
||||
type: giftCard.type
|
||||
? { label: giftCard.type.value, value: giftCard.type.id }
|
||||
: null,
|
||||
tags: giftCard.tags.map((t) => t.value),
|
||||
images: giftCard.images.map((img) => ({ url: img.url })),
|
||||
thumbnail,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { useAdminProducts } from "medusa-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import BackButton from "../../../components/atoms/back-button"
|
||||
import Spinner from "../../../components/atoms/spinner"
|
||||
import Toaster from "../../../components/declarative-toaster"
|
||||
import FormToasterContainer from "../../../components/molecules/form-toaster"
|
||||
import { checkForDirtyState } from "../../../utils/form-helpers"
|
||||
import {
|
||||
GiftCardFormProvider,
|
||||
useGiftCardForm,
|
||||
} from "./form/gift-card-form-context"
|
||||
import { giftCardToFormValuesMapper } from "./form/mappers"
|
||||
import Denominations from "./sections/denominations"
|
||||
import Images from "./sections/images"
|
||||
import Information from "./sections/information"
|
||||
import GiftCardDenominationsSection from "../../../components/organisms/gift-card-denominations-section"
|
||||
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
|
||||
import ProductGeneralSection from "../../../components/organisms/product-general-section"
|
||||
import ProductMediaSection from "../../../components/organisms/product-media-section"
|
||||
import ProductRawSection from "../../../components/organisms/product-raw-section"
|
||||
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
|
||||
import { getErrorStatus } from "../../../utils/get-error-status"
|
||||
|
||||
const ManageGiftCard = () => {
|
||||
const { products } = useAdminProducts(
|
||||
const Manage = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { products, error } = useAdminProducts(
|
||||
{
|
||||
is_giftcard: true,
|
||||
},
|
||||
@@ -24,7 +23,7 @@ const ManageGiftCard = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const giftCard = products?.[0]
|
||||
const giftCard = products?.[0] as Product | undefined
|
||||
|
||||
if (!giftCard) {
|
||||
return (
|
||||
@@ -34,72 +33,42 @@ const ManageGiftCard = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorStatus = getErrorStatus(error)
|
||||
|
||||
if (errorStatus) {
|
||||
// If the product is not found, redirect to the 404 page
|
||||
if (errorStatus.status === 404) {
|
||||
navigate("/404")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Let the error boundary handle the error
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<GiftCardFormProvider
|
||||
giftCard={giftCardToFormValuesMapper(giftCard)}
|
||||
giftCardId={giftCard.id}
|
||||
>
|
||||
<div className="gap-y-large pb-xlarge flex flex-col">
|
||||
<Information giftCard={giftCard} />
|
||||
<Denominations giftCard={giftCard} />
|
||||
<Images />
|
||||
<div className="pb-5xlarge">
|
||||
<BackButton
|
||||
path="/a/gift-cards"
|
||||
label="Back to Gift Cards"
|
||||
className="mb-xsmall"
|
||||
/>
|
||||
<div className="gap-x-base grid grid-cols-12">
|
||||
<div className="gap-y-xsmall col-span-8 flex flex-col">
|
||||
<ProductGeneralSection product={giftCard} />
|
||||
<GiftCardDenominationsSection giftCard={giftCard} />
|
||||
<ProductAttributesSection product={giftCard} />
|
||||
<ProductRawSection product={giftCard} />
|
||||
</div>
|
||||
<div className="gap-y-xsmall col-span-4 flex flex-col">
|
||||
<ProductThumbnailSection product={giftCard} />
|
||||
<ProductMediaSection product={giftCard} />
|
||||
</div>
|
||||
</div>
|
||||
<UpdateNotification />
|
||||
</GiftCardFormProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TOAST_ID = "edit-gc-dirty"
|
||||
|
||||
const UpdateNotification = ({ isLoading = false }) => {
|
||||
const {
|
||||
form: { formState },
|
||||
onUpdate,
|
||||
resetForm,
|
||||
additionalDirtyState,
|
||||
} = useGiftCardForm()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [blocking, setBlocking] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(setBlocking, 300, false)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [])
|
||||
|
||||
const isDirty = checkForDirtyState(
|
||||
formState.dirtyFields,
|
||||
additionalDirtyState
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!blocking) {
|
||||
setVisible(isDirty)
|
||||
}
|
||||
|
||||
return () => {
|
||||
toast.dismiss(TOAST_ID)
|
||||
}
|
||||
}, [isDirty])
|
||||
|
||||
return (
|
||||
<Toaster
|
||||
visible={visible}
|
||||
duration={Infinity}
|
||||
id={TOAST_ID}
|
||||
position="bottom-right"
|
||||
>
|
||||
<FormToasterContainer isLoading={isLoading}>
|
||||
<FormToasterContainer.Actions>
|
||||
<FormToasterContainer.ActionButton onClick={onUpdate}>
|
||||
Save
|
||||
</FormToasterContainer.ActionButton>
|
||||
<FormToasterContainer.DiscardButton onClick={resetForm}>
|
||||
Discard
|
||||
</FormToasterContainer.DiscardButton>
|
||||
</FormToasterContainer.Actions>
|
||||
</FormToasterContainer>
|
||||
</Toaster>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManageGiftCard
|
||||
export default Manage
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Product, ProductVariant } from "@medusajs/medusa"
|
||||
import {
|
||||
useAdminDeleteVariant,
|
||||
useAdminStore,
|
||||
useAdminUpdateVariant,
|
||||
} from "medusa-react"
|
||||
import React, { useState } from "react"
|
||||
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
|
||||
import AddDenominationModal from "../../../../components/organisms/add-denomination-modal"
|
||||
import BodyCard from "../../../../components/organisms/body-card"
|
||||
import EditDenominationsModal from "../../../../components/organisms/edit-denominations-modal"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||
import DenominationTable from "../denomination-table"
|
||||
|
||||
type DenominationsProps = {
|
||||
giftCard: Product
|
||||
}
|
||||
|
||||
const Denominations: React.FC<DenominationsProps> = ({ giftCard }) => {
|
||||
const [editDenom, setEditDenom] = useState<Omit<
|
||||
ProductVariant,
|
||||
"beforeInsert"
|
||||
> | null>(null)
|
||||
const [addDenom, setAddDenom] = useState(false)
|
||||
|
||||
const { store } = useAdminStore()
|
||||
const updateGiftCardVariant = useAdminUpdateVariant(giftCard.id)
|
||||
const deleteGiftCardVariant = useAdminDeleteVariant(giftCard.id)
|
||||
const notification = useNotification()
|
||||
|
||||
const currencyCodes =
|
||||
store?.currencies
|
||||
.filter((currency) => currency.code !== store.default_currency_code)
|
||||
.map((currency) => currency.code) || []
|
||||
|
||||
const submitDenomations = (denoms) => {
|
||||
if (!denoms.length) {
|
||||
// if a update would result in the variant having 0 prices, then we delete it instead
|
||||
deleteGiftCardVariant.mutate(editDenom!.id, {
|
||||
onSuccess: () => {
|
||||
notification(
|
||||
"Success",
|
||||
"Successfully updated denominations",
|
||||
"success"
|
||||
)
|
||||
setEditDenom(null)
|
||||
},
|
||||
onError: (err) => notification("Error", getErrorMessage(err), "error"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updateGiftCardVariant.mutate(
|
||||
{
|
||||
variant_id: editDenom!.id,
|
||||
prices: denoms.map(({ amount, currency_code }) => ({
|
||||
amount,
|
||||
currency_code,
|
||||
})),
|
||||
title: editDenom!.title,
|
||||
inventory_quantity: editDenom!.inventory_quantity,
|
||||
options: editDenom!.options.map((opt) => ({
|
||||
option_id: opt.option_id,
|
||||
value: opt.value,
|
||||
})),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notification(
|
||||
"Success",
|
||||
"Successfully updated denominations",
|
||||
"success"
|
||||
)
|
||||
setEditDenom(null)
|
||||
},
|
||||
onError: (err) => notification("Error", getErrorMessage(err), "error"),
|
||||
}
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<BodyCard
|
||||
title="Denominations"
|
||||
subtitle="Manage your denominations"
|
||||
className={"h-auto w-full"}
|
||||
actionables={[
|
||||
{
|
||||
label: "Add Denomination",
|
||||
onClick: () => setAddDenom(true),
|
||||
icon: <PlusIcon size={20} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DenominationTable
|
||||
giftCardId={giftCard.id}
|
||||
denominations={giftCard.variants || []}
|
||||
defaultCurrency={store?.default_currency_code || ""}
|
||||
setEditDenom={setEditDenom}
|
||||
/>
|
||||
</BodyCard>
|
||||
{editDenom && (
|
||||
<EditDenominationsModal
|
||||
currencyCodes={store?.currencies.map((c) => c.code)}
|
||||
onSubmit={submitDenomations}
|
||||
defaultDenominations={editDenom.prices.map((p) => ({
|
||||
amount: p.amount,
|
||||
currency_code: p.currency_code,
|
||||
id: p.id,
|
||||
}))}
|
||||
handleClose={() => setEditDenom(null)}
|
||||
/>
|
||||
)}
|
||||
{addDenom && (
|
||||
<AddDenominationModal
|
||||
giftCard={giftCard}
|
||||
handleClose={() => setAddDenom(false)}
|
||||
storeCurrency={store?.default_currency_code!}
|
||||
currencyCodes={currencyCodes}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Denominations
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Controller, useFieldArray } from "react-hook-form"
|
||||
import FileUploadField from "../../../../components/atoms/file-upload-field"
|
||||
import BodyCard from "../../../../components/organisms/body-card"
|
||||
import RadioGroup from "../../../../components/organisms/radio-group"
|
||||
import ImageTable, {
|
||||
ImageTableDataType,
|
||||
} from "../../../../components/templates/image-table"
|
||||
import { nestedForm } from "../../../../utils/nested-form"
|
||||
import { useGiftCardForm } from "../form/gift-card-form-context"
|
||||
|
||||
const Images = () => {
|
||||
const { form, setImageDirtyState } = useGiftCardForm()
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "images",
|
||||
})
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
setImageDirtyState(true)
|
||||
remove(index)
|
||||
}
|
||||
|
||||
const handleFilesChosen = (files: File[]) => {
|
||||
if (files.length) {
|
||||
const toAppend = files.map((file) => ({
|
||||
url: URL.createObjectURL(file),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
nativeFile: file,
|
||||
}))
|
||||
|
||||
append(toAppend)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyCard title="Images" subtitle="Add up to 10 images to your product">
|
||||
<div className="mt-base">
|
||||
<Controller
|
||||
name="thumbnail"
|
||||
control={form.control}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
value={value ? `${value}` : undefined}
|
||||
onValueChange={(value) => {
|
||||
onChange(parseInt(value))
|
||||
}}
|
||||
>
|
||||
<ImageTable
|
||||
data={fields as ImageTableDataType[]}
|
||||
form={nestedForm(form, "images")}
|
||||
onDelete={handleRemove}
|
||||
/>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id} className="flex items-center">
|
||||
<input
|
||||
className="hidden"
|
||||
{...form.register(`images.${index}.url`)}
|
||||
defaultValue={field.url}
|
||||
/>
|
||||
{field.nativeFile && (
|
||||
<>
|
||||
<input
|
||||
className="hidden"
|
||||
{...form.register(`images.${index}.name`)}
|
||||
defaultValue={field.name}
|
||||
/>
|
||||
<input
|
||||
className="hidden"
|
||||
{...form.register(`images.${index}.size`)}
|
||||
defaultValue={field.size}
|
||||
/>
|
||||
<Controller
|
||||
name={`images.${index}.nativeFile`}
|
||||
control={form.control}
|
||||
defaultValue={field.nativeFile}
|
||||
render={() => <></>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</RadioGroup.Root>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2xlarge">
|
||||
<FileUploadField
|
||||
onFileChosen={handleFilesChosen}
|
||||
placeholder="1200 x 1600 (3:4) recommended, up to 10MB each"
|
||||
filetypes={["image/gif", "image/jpeg", "image/png", "image/webp"]}
|
||||
className="py-large"
|
||||
/>
|
||||
</div>
|
||||
</BodyCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Images
|
||||
@@ -1,227 +0,0 @@
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import {
|
||||
useAdminDeleteProduct,
|
||||
useAdminProductTypes,
|
||||
useAdminUpdateProduct,
|
||||
} from "medusa-react"
|
||||
import React from "react"
|
||||
import { Controller } from "react-hook-form"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
import UnpublishIcon from "../../../../components/fundamentals/icons/unpublish-icon"
|
||||
import Input from "../../../../components/molecules/input"
|
||||
import Select from "../../../../components/molecules/select"
|
||||
import StatusSelector from "../../../../components/molecules/status-selector"
|
||||
import TagInput from "../../../../components/molecules/tag-input"
|
||||
import TextArea from "../../../../components/molecules/textarea"
|
||||
import BodyCard from "../../../../components/organisms/body-card"
|
||||
import DetailsCollapsible from "../../../../components/organisms/details-collapsible"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||
import FormValidator from "../../../../utils/form-validator"
|
||||
import { useGiftCardForm } from "../form/gift-card-form-context"
|
||||
|
||||
type InformationProps = {
|
||||
giftCard: Omit<Product, "beforeInsert">
|
||||
}
|
||||
|
||||
const Information: React.FC<InformationProps> = ({ giftCard }) => {
|
||||
const {
|
||||
form: {
|
||||
register,
|
||||
setValue,
|
||||
control,
|
||||
formState: { errors },
|
||||
},
|
||||
} = useGiftCardForm()
|
||||
const navigate = useNavigate()
|
||||
const notification = useNotification()
|
||||
const { product_types } = useAdminProductTypes(undefined, {
|
||||
cacheTime: 0,
|
||||
})
|
||||
|
||||
const typeOptions =
|
||||
product_types?.map((tag) => ({ label: tag.value, value: tag.id })) || []
|
||||
|
||||
const updateGiftCard = useAdminUpdateProduct(giftCard.id)
|
||||
const deleteGiftCard = useAdminDeleteProduct(giftCard.id)
|
||||
|
||||
const setNewType = (value: string) => {
|
||||
const newType = {
|
||||
label: value,
|
||||
value,
|
||||
}
|
||||
|
||||
typeOptions.push(newType)
|
||||
setValue("type", newType)
|
||||
|
||||
return newType
|
||||
}
|
||||
|
||||
const onUpdate = () => {
|
||||
updateGiftCard.mutate(
|
||||
{
|
||||
// @ts-ignore
|
||||
status: giftCard.status === "draft" ? "published" : "draft",
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notification("Success", "Gift card updated successfully", "success")
|
||||
},
|
||||
onError: (error) => {
|
||||
notification("Error", getErrorMessage(error), "error")
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
deleteGiftCard.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
navigate("/a/gift-cards")
|
||||
notification("Success", "Gift card updated successfully", "success")
|
||||
},
|
||||
onError: (error) => {
|
||||
notification("Error", getErrorMessage(error), "error")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyCard
|
||||
title="Product information"
|
||||
subtitle="Manage the settings for your Gift Card"
|
||||
className={"h-auto w-full"}
|
||||
status={
|
||||
<GiftCardStatusSelector
|
||||
currentStatus={giftCard.status}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
}
|
||||
actionables={[
|
||||
{
|
||||
label:
|
||||
giftCard?.status !== "published"
|
||||
? "Publish Gift Card"
|
||||
: "Unpublish Gift Card",
|
||||
onClick: onUpdate,
|
||||
icon: <UnpublishIcon size="16" />,
|
||||
},
|
||||
{
|
||||
label: "Delete Gift Card",
|
||||
onClick: onDelete,
|
||||
variant: "danger",
|
||||
icon: <TrashIcon size="16" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="flex flex-col space-y-6">
|
||||
<div className="gap-large grid grid-cols-2">
|
||||
<Input
|
||||
label="Name"
|
||||
placeholder="Add name"
|
||||
required
|
||||
defaultValue={giftCard?.title}
|
||||
{...register("title", {
|
||||
required: FormValidator.required("Name"),
|
||||
pattern: FormValidator.whiteSpaceRule("Name"),
|
||||
minLength: FormValidator.minOneCharRule("Name"),
|
||||
})}
|
||||
errors={errors}
|
||||
/>
|
||||
<Input
|
||||
label="Subtitle"
|
||||
placeholder="Add a subtitle"
|
||||
{...register("subtitle", {
|
||||
pattern: FormValidator.whiteSpaceRule("Subtitle"),
|
||||
minLength: FormValidator.minOneCharRule("Subtitle"),
|
||||
})}
|
||||
errors={errors}
|
||||
/>
|
||||
<TextArea
|
||||
label="Description"
|
||||
placeholder="Add a description"
|
||||
{...register("description", {
|
||||
pattern: FormValidator.whiteSpaceRule("Description"),
|
||||
minLength: FormValidator.minOneCharRule("Description"),
|
||||
})}
|
||||
errors={errors}
|
||||
/>
|
||||
</div>
|
||||
<DetailsCollapsible
|
||||
triggerProps={{ className: "ml-2" }}
|
||||
contentProps={{
|
||||
forceMount: true,
|
||||
}}
|
||||
>
|
||||
<div className="gap-large grid grid-cols-2">
|
||||
<Input
|
||||
label="Handle"
|
||||
placeholder="Product handle"
|
||||
{...register("handle", {
|
||||
pattern: FormValidator.whiteSpaceRule("Handle"),
|
||||
minLength: FormValidator.minOneCharRule("Handle"),
|
||||
})}
|
||||
tooltipContent="URL of the product"
|
||||
errors={errors}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<Select
|
||||
label="Type"
|
||||
placeholder="Select type..."
|
||||
options={typeOptions}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
isCreatable
|
||||
onCreateOption={(value) => {
|
||||
return setNewType(value)
|
||||
}}
|
||||
clearSelected
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="tags"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<TagInput
|
||||
label="Tags (separated by comma)"
|
||||
className="w-full"
|
||||
placeholder="Spring, Summer..."
|
||||
onChange={onChange}
|
||||
values={value || []}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
control={control}
|
||||
/>
|
||||
</div>
|
||||
</DetailsCollapsible>
|
||||
</div>
|
||||
</BodyCard>
|
||||
)
|
||||
}
|
||||
|
||||
const GiftCardStatusSelector = ({
|
||||
currentStatus,
|
||||
onUpdate,
|
||||
}: {
|
||||
currentStatus: "draft" | "proposed" | "published" | "rejected"
|
||||
onUpdate: () => void
|
||||
}) => {
|
||||
return (
|
||||
<StatusSelector
|
||||
activeState="Published"
|
||||
draftState="Draft"
|
||||
isDraft={currentStatus === "draft"}
|
||||
onChange={onUpdate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Information
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FormImage, Option } from "../../../../types/shared"
|
||||
|
||||
export type ManageGiftCardFormData = {
|
||||
title: string
|
||||
handle?: string
|
||||
subtitle?: string | null
|
||||
description?: string | null
|
||||
type: Option | null
|
||||
tags?: string[]
|
||||
images: FormImage[]
|
||||
thumbnail: number
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SalesChannel, StockLocationExpandedDTO } from "@medusajs/medusa"
|
||||
import {
|
||||
useAdminAddLocationToSalesChannel,
|
||||
useAdminRemoveLocationFromSalesChannel,
|
||||
useAdminRemoveLocationFromSalesChannel
|
||||
} from "medusa-react"
|
||||
|
||||
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import SalesChannelsModal from "../../../../products/components/sales-channels-modal"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
|
||||
const EditSalesChannels = ({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SalesChannel, StockLocationExpandedDTO } from "@medusajs/medusa"
|
||||
import { useFieldArray } from "react-hook-form"
|
||||
import SalesChannelsModal from "../../../../../components/forms/product/sales-channels-modal"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import SalesChannelsList from "../../../../../components/molecules/sales-channels-list"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import { NestedForm } from "../../../../../utils/nested-form"
|
||||
import SalesChannelsModal from "../../../../products/components/sales-channels-modal"
|
||||
import { AddSalesChannelsFormType } from "../../../../products/new/add-sales-channels"
|
||||
|
||||
const SalesChannelsForm = ({
|
||||
|
||||
@@ -4,12 +4,12 @@ import clsx from "clsx"
|
||||
import { useAdminShippingOptions } from "medusa-react"
|
||||
import { useMemo } from "react"
|
||||
import { Controller, useWatch } from "react-hook-form"
|
||||
import PriceFormInput from "../../../../components/forms/general/prices-form/price-form-input"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
import { NextSelect } from "../../../../components/molecules/select/next-select"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import { formatAmountWithSymbol } from "../../../../utils/prices"
|
||||
import PriceFormInput from "../../../products/components/prices-form/price-form-input"
|
||||
|
||||
export type ShippingFormType = {
|
||||
option: {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useAdminProduct } from "medusa-react"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import BackButton from "../../../components/atoms/back-button"
|
||||
import Spinner from "../../../components/atoms/spinner"
|
||||
import ProductAttributesSection from "../../../components/organisms/product-attributes-section"
|
||||
import ProductGeneralSection from "../../../components/organisms/product-general-section"
|
||||
import ProductMediaSection from "../../../components/organisms/product-media-section"
|
||||
import ProductRawSection from "../../../components/organisms/product-raw-section"
|
||||
import ProductThumbnailSection from "../../../components/organisms/product-thumbnail-section"
|
||||
import ProductVariantsSection from "../../../components/organisms/product-variants-section"
|
||||
import { getErrorStatus } from "../../../utils/get-error-status"
|
||||
import AttributesSection from "./sections/attributes"
|
||||
import GeneralSection from "./sections/general"
|
||||
import MediaSection from "./sections/media"
|
||||
import RawSection from "./sections/raw"
|
||||
import ThumbnailSection from "./sections/thumbnail"
|
||||
import VariantsSection from "./sections/variants"
|
||||
|
||||
const Edit = () => {
|
||||
const { id } = useParams()
|
||||
@@ -17,13 +17,9 @@ const Edit = () => {
|
||||
const { product, status, error } = useAdminProduct(id || "")
|
||||
|
||||
if (error) {
|
||||
let message = "An unknown error occurred"
|
||||
|
||||
const errorStatus = getErrorStatus(error)
|
||||
|
||||
if (errorStatus) {
|
||||
message = errorStatus.message
|
||||
|
||||
// If the product is not found, redirect to the 404 page
|
||||
if (errorStatus.status === 404) {
|
||||
navigate("/404")
|
||||
@@ -53,14 +49,14 @@ const Edit = () => {
|
||||
/>
|
||||
<div className="gap-x-base grid grid-cols-12">
|
||||
<div className="gap-y-xsmall col-span-8 flex flex-col">
|
||||
<GeneralSection product={product} />
|
||||
<VariantsSection product={product} />
|
||||
<AttributesSection product={product} />
|
||||
<RawSection product={product} />
|
||||
<ProductGeneralSection product={product} />
|
||||
<ProductVariantsSection product={product} />
|
||||
<ProductAttributesSection product={product} />
|
||||
<ProductRawSection product={product} />
|
||||
</div>
|
||||
<div className="gap-y-xsmall col-span-4 flex flex-col">
|
||||
<ThumbnailSection product={product} />
|
||||
<MediaSection product={product} />
|
||||
<ProductThumbnailSection product={product} />
|
||||
<ProductMediaSection product={product} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useAdminStore } from "medusa-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useFieldArray } from "react-hook-form"
|
||||
import Switch from "../../../components/atoms/switch"
|
||||
import SalesChannelsModal from "../../../components/forms/product/sales-channels-modal"
|
||||
import Button from "../../../components/fundamentals/button"
|
||||
import ChannelsIcon from "../../../components/fundamentals/icons/channels-icon"
|
||||
import SalesChannelsDisplay from "../../../components/molecules/sales-channels-display"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
import { NestedForm } from "../../../utils/nested-form"
|
||||
import SalesChannelsModal from "../components/sales-channels-modal"
|
||||
|
||||
export type AddSalesChannelsFormType = {
|
||||
channels: SalesChannel[]
|
||||
|
||||
@@ -2,6 +2,13 @@ import clsx from "clsx"
|
||||
import { useCallback, useContext, useEffect, useMemo } from "react"
|
||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { CustomsFormType } from "../../../../components/forms/product/customs-form"
|
||||
import { DimensionsFormType } from "../../../../components/forms/product/dimensions-form"
|
||||
import CreateFlowVariantForm, {
|
||||
CreateFlowVariantFormType
|
||||
} from "../../../../components/forms/product/variant-form/create-flow-variant-form"
|
||||
import { VariantOptionType } from "../../../../components/forms/product/variant-form/variant-select-options-form"
|
||||
import useCheckOptions from "../../../../components/forms/product/variant-form/variant-select-options-form/hooks"
|
||||
import Button from "../../../../components/fundamentals/button"
|
||||
import PlusIcon from "../../../../components/fundamentals/icons/plus-icon"
|
||||
import TrashIcon from "../../../../components/fundamentals/icons/trash-icon"
|
||||
@@ -9,19 +16,12 @@ import IconTooltip from "../../../../components/molecules/icon-tooltip"
|
||||
import InputField from "../../../../components/molecules/input"
|
||||
import Modal from "../../../../components/molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
LayeredModalContext
|
||||
} from "../../../../components/molecules/modal/layered-modal"
|
||||
import TagInput from "../../../../components/molecules/tag-input"
|
||||
import { useDebounce } from "../../../../hooks/use-debounce"
|
||||
import useToggleState from "../../../../hooks/use-toggle-state"
|
||||
import { NestedForm } from "../../../../utils/nested-form"
|
||||
import { CustomsFormType } from "../../components/customs-form"
|
||||
import { DimensionsFormType } from "../../components/dimensions-form"
|
||||
import CreateFlowVariantForm, {
|
||||
CreateFlowVariantFormType,
|
||||
} from "../../components/variant-form/create-flow-variant-form"
|
||||
import { VariantOptionType } from "../../components/variant-form/variant-select-options-form"
|
||||
import useCheckOptions from "../../components/variant-form/variant-select-options-form/hooks"
|
||||
import NewVariant from "./new-variant"
|
||||
|
||||
type ProductOptionType = {
|
||||
|
||||
@@ -4,6 +4,12 @@ import { useContext, useEffect, useRef } from "react"
|
||||
import { useDrag, useDrop } from "react-dnd"
|
||||
import { useForm } from "react-hook-form"
|
||||
import Tooltip from "../../../../../components/atoms/tooltip"
|
||||
import { CustomsFormType } from "../../../../../components/forms/product/customs-form"
|
||||
import { DimensionsFormType } from "../../../../../components/forms/product/dimensions-form"
|
||||
import CreateFlowVariantForm, {
|
||||
CreateFlowVariantFormType
|
||||
} from "../../../../../components/forms/product/variant-form/create-flow-variant-form"
|
||||
import { VariantOptionValueType } from "../../../../../components/forms/product/variant-form/variant-select-options-form"
|
||||
import Button from "../../../../../components/fundamentals/button"
|
||||
import CheckCircleFillIcon from "../../../../../components/fundamentals/icons/check-circle-fill-icon"
|
||||
import EditIcon from "../../../../../components/fundamentals/icons/edit-icon"
|
||||
@@ -14,17 +20,11 @@ import Actionables from "../../../../../components/molecules/actionables"
|
||||
import IconTooltip from "../../../../../components/molecules/icon-tooltip"
|
||||
import Modal from "../../../../../components/molecules/modal"
|
||||
import LayeredModal, {
|
||||
LayeredModalContext,
|
||||
LayeredModalContext
|
||||
} from "../../../../../components/molecules/modal/layered-modal"
|
||||
import useImperativeDialog from "../../../../../hooks/use-imperative-dialog"
|
||||
import useToggleState from "../../../../../hooks/use-toggle-state"
|
||||
import { DragItem } from "../../../../../types/shared"
|
||||
import { CustomsFormType } from "../../../components/customs-form"
|
||||
import { DimensionsFormType } from "../../../components/dimensions-form"
|
||||
import CreateFlowVariantForm, {
|
||||
CreateFlowVariantFormType,
|
||||
} from "../../../components/variant-form/create-flow-variant-form"
|
||||
import { VariantOptionValueType } from "../../../components/variant-form/variant-select-options-form"
|
||||
|
||||
const ItemTypes = {
|
||||
CARD: "card",
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import AddSalesChannelsForm, {
|
||||
AddSalesChannelsFormType,
|
||||
} from "./add-sales-channels"
|
||||
import AddVariantsForm, { AddVariantsFormType } from "./add-variants"
|
||||
import { AdminPostProductsReq, ProductVariant } from "@medusajs/medusa"
|
||||
import CustomsForm, { CustomsFormType } from "../components/customs-form"
|
||||
import DimensionsForm, {
|
||||
DimensionsFormType,
|
||||
} from "../components/dimensions-form"
|
||||
import DiscountableForm, {
|
||||
DiscountableFormType,
|
||||
} from "../components/discountable-form"
|
||||
import { FormImage, ProductStatus } from "../../../types/shared"
|
||||
import GeneralForm, { GeneralFormType } from "../components/general-form"
|
||||
import MediaForm, { MediaFormType } from "../components/media-form"
|
||||
import OrganizeForm, { OrganizeFormType } from "../components/organize-form"
|
||||
import ThumbnailForm, { ThumbnailFormType } from "../components/thumbnail-form"
|
||||
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 Accordion from "../../../components/organisms/accordion"
|
||||
import Button from "../../../components/fundamentals/button"
|
||||
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
|
||||
import FeatureToggle from "../../../components/fundamentals/feature-toggle"
|
||||
import FocusModal from "../../../components/molecules/modal/focus-modal"
|
||||
import { PricesFormType } from "../components/prices-form"
|
||||
import { getErrorMessage } from "../../../utils/error-messages"
|
||||
import { nestedForm } from "../../../utils/nested-form"
|
||||
import { prepareImages } from "../../../utils/images"
|
||||
import { useEffect } from "react"
|
||||
import { useFeatureFlag } from "../../../providers/feature-flag-provider"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
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
|
||||
@@ -357,14 +357,14 @@ const createPayload = (
|
||||
mid_code: data.customs.mid_code || undefined,
|
||||
type: data.organize.type
|
||||
? {
|
||||
value: data.organize.type.label,
|
||||
id: data.organize.type.value,
|
||||
}
|
||||
value: data.organize.type.label,
|
||||
id: data.organize.type.value,
|
||||
}
|
||||
: undefined,
|
||||
tags: data.organize.tags
|
||||
? data.organize.tags.map((t) => ({
|
||||
value: t,
|
||||
}))
|
||||
value: t,
|
||||
}))
|
||||
: undefined,
|
||||
categories: data.organize.categories?.length
|
||||
? data.organize.categories.map((id) => ({ id }))
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Region } from "@medusajs/medusa"
|
||||
import { Controller, UseFormReturn } from "react-hook-form"
|
||||
import IncludesTaxTooltip from "../../../../../components/atoms/includes-tax-tooltip"
|
||||
import Switch from "../../../../../components/atoms/switch"
|
||||
import PriceFormInput from "../../../../../components/forms/general/prices-form/price-form-input"
|
||||
import InputHeader from "../../../../../components/fundamentals/input-header"
|
||||
import InputField from "../../../../../components/molecules/input"
|
||||
import { NextSelect } from "../../../../../components/molecules/select/next-select"
|
||||
import { Option, ShippingOptionPriceType } from "../../../../../types/shared"
|
||||
import FormValidator from "../../../../../utils/form-validator"
|
||||
import PriceFormInput from "../../../../products/components/prices-form/price-form-input"
|
||||
import { useShippingOptionFormData } from "./use-shipping-option-form-data"
|
||||
|
||||
type Requirement = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AdminPostProductsProductReq,
|
||||
AdminPostProductsProductVariantsReq,
|
||||
AdminPostProductsProductVariantsVariantReq,
|
||||
Product,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
useAdminCreateVariant,
|
||||
@@ -12,11 +13,11 @@ import {
|
||||
useAdminUpdateVariant,
|
||||
} from "medusa-react"
|
||||
|
||||
import { getErrorMessage } from "../../../../utils/error-messages"
|
||||
import { removeNullish } from "../../../../utils/remove-nullish"
|
||||
import useImperativeDialog from "../../../../hooks/use-imperative-dialog"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import useNotification from "../../../../hooks/use-notification"
|
||||
import { getErrorMessage } from "../utils/error-messages"
|
||||
import { removeNullish } from "../utils/remove-nullish"
|
||||
import useImperativeDialog from "./use-imperative-dialog"
|
||||
import useNotification from "./use-notification"
|
||||
|
||||
const useEditProductActions = (productId: string) => {
|
||||
const dialog = useImperativeDialog()
|
||||
@@ -49,7 +50,7 @@ const useEditProductActions = (productId: string) => {
|
||||
|
||||
const onAddVariant = (
|
||||
payload: AdminPostProductsProductVariantsReq,
|
||||
onSuccess: (variantRes) => void,
|
||||
onSuccess: (variantRes: Product) => void,
|
||||
successMessage = "Variant was created successfully"
|
||||
) => {
|
||||
addVariant.mutate(payload, {
|
||||
Reference in New Issue
Block a user